Versiyon Yönetimi

Paylaşımlı kütüphanelerin kodu zaman içerisinde değişebilmekte ve neticesinde yeni versiyonları çıkmaktadır. Yapılan değişikliğin derececesine göre versiyonları iki gruba ayırabiliriz.

  • Major, önceki versiyonlarla uyumluluğun (compatibility) korunmadığı, genel olarak kapsamlı değişikliklerin yapıldığı versiyonlardır.

  • Minör, önceki versiyonlarla uyumluluğun korunduğu versiyonlardır.

Örneğin, kütüphanenin arayüzünden bir fonksiyonun çıkarılması durumunda, kütüphanenin eski versiyonlarında bu fonksiyonu kullanan uygulamalar artık yeni halini kullanamayacak ve ABI (Application Binary Interface) uyumluluğu kırılacaktır. Benzer şekilde var olan fonksiyonların ürettikleri sonuçların veya parametrik yapılarının değiştirilmesi durumunda da geçmişe dönük uyumluluk kırılacaktır. Buna karşın, çoğu durumda bu tip değişikliklere ihtiyaç duyulmamaktadır. Kütüphaneye yeni fonksiyonlar eklenmesi veya var olanların parametrik yapıları korunarak iyileştirilmeleri durumunda geçmişe uyumluluğun korunduğu yeni versiyonları çıkarılmaktadır.

Paylaşımlı kütüphanelerin versiyonlarının kontrolünde iki yöntem kullanılabilmektedir. Sırasıyla bu yöntemlere bakalım.

İsimlendirme Geleneği (Naming Conventions)

Kütüphaneler, versiyonlarını da gösterecek biçimde libisim.so.[major].[minör].[revizyon] şeklinde isimlendirilmektedir. Revizyon numarası her durumda kullanılmayabilir. Genel yaklaşım versiyon arttıkça versiyon numaralarının değerini 1 arttırmak şeklindedir. Örneğin, sistemimizdeki Qt kütüphanesinin aşağıdaki gibi isimlendirildiğini görmekteyiz.

libQtCore.so.4.8.6

Bir uygulamanın, bağımlı olduğu kütüphanenin tüm minör versiyonlarıyla çalışması istenmektedir. Uygulamanın bağımlılık listesine kütüphanenin gerçek isminin yazılması durumunda, uygulama kütüphanenin o versiyonuna katı bir şekilde bağlanacak ve başka uyumlu versiyonlarıyla çalışamayacaktır. Bir örnek üzerinden bu durumu inceleyelim.

driver.c:

#include <stdio.h>

void foo(int);

int main() {
    int ret = foo(16);
    printf("%d\n", ret);
    return 0;
}

test.c:

void foo(int val) {
    return val / 2;
}

Kütüphanemizi isimlendirme kurallarına uygun isimlendirip, ardından uygulamamızı derleyip çalıştırabiliriz.

$ gcc -fPIC -shared -olibtest.so.1.0.0 test.c
$ gcc -odriver driver.c libtest.so.1.0.0
$ LD_LIBRARY_PATH=. ./driver
8

Uygulamımızın bağımlılık listesinde beklediğimiz üzere libtest.so.1.0.0 kütüphanesini görmekteyiz.

$ readelf -d driver | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libtest.so.1.0.0]

Kütüphane kodunda, uyumluluğu gözeterek, bir iyileştirme yaptığımızı ve kütüphanenin revizyon numarasını 1 arttırarak, libtest.so.1.0.1 adıyla kütüphaneyi yeniden derlediğimizi düşünelim

gcc -fPIC -shared -olibtest.so.1.0.1 test.c

Bu durumda driver uygulamasının yeni kütüphaneyi kullanamayacağı açıktır. Uygulamanın yeni kütüphaneye statik olarak yeniden bağlanması gerekmektedir. Bu problemi gidermek için kütüphanelere soname denilen mantıksal isimler verilmektedir. Mantıksal isimler kütüphane ismiyle beraber yalnız major versiyon numarasını içermektedir. Bu sayede birbiriyle uyumlu olan tüm versiyonlar aynı mantıksal ismi paylaşmakta ve bu şekilde ortak bir isim alanı oluşturulmaktadır. Bu mantıksal isimler kütüphaneler oluşturulurken, bağlayıcı tarafından kütüphaneye dosyasına yerleştirilmektedir. Bu amaçla, ELF formatında DT_SONAME alanı bulunmaktadır. Bir uygulamanın kütüphaneye bağlanması aşamasında, uygulamanın bağımlılık listesine, kütüphanenin adı değil mantıksal ismi (soname) yazılmaktadır. Bu kez kütüphanemize mantıksal bir isim atayarak yeniden derleyelim.

$ gcc -fPIC -shared -Wl,-soname,libtest.so.1 -olibtest.so.1.0.0 test.c
$ gcc -odriver driver.c libtest.so.1.0.0

Kütüphanenin mantıksal ismini aşağıdaki gibi öğrenebiliriz.

$ readelf -d libtest.so.1.0.0 | grep SONAME
 0x000000000000000e (SONAME)             Library soname: [libtest.so.1]

Uygulamayı çalıştırmayı denediğimizde, bağlayıcının libtest.so.1.0.0 dosyasını değil libtest.so.1 dosyasını aradığını görmekteyiz.

$ ./driver
$ LD_LIBRARY_PATH=. ./driver: error while loading shared libraries: libtest.so.1: cannot open shared object file: No such file or directory

Bu durumda gerçek kütüpnane dosyasına, kütüphanenin mantıksal isminde bir sembolink link oluşturabiliriz.

$ ln -s libtest.so.1.0.0 libtest.so.1
$ LD_LIBRARY_PATH=. ./driver
8

Bu kez uygulamanın çalıştığını görmekteyiz. Şimdi daha önce hedeflediğimiz gibi, uyumluluğu koruyarak, kütüphanede bir değişiklik yapıp revizyon numarasını 1 arttıralım.

test.c:

int foo(int val) {
    puts("İyileştirilmiş versiyon");
    return val >> 1;
}
gcc -fPIC -shared -Wl,-soname,libtest.so.1 -olibtest.so.1.0.1 test.c

Uygulama üzerinde herhangi bir işlem yapmaksızın yalnız sembolik bağlantıyı yeni kütüphaneyi gösterecek şekilde değiştirelim.

$ rm libtest.so.1
$ ln -s libtest.so.1.0.1 libtest.so.1

Uygulamayı yeniden çalıştırdığımızda kütüphanenin yeni versiyonunu kullandığını görmekteyiz.

LD_LIBRARY_PATH=. ./driver
İyileştirilmiş versiyon
8

Ayrıca derleme zamanında kütüphanenin tam ismini yazmak yerine versiyon bilgisi içermeyen bir bağlayıcı adı (linker name) kullanılmaktadır. Bu sebeple kütüphanenin yalnız adını içeren bir sembolik link daha oluşturulmaktadır. Bu link gerçek kütüphaneyi gösterebilmesine karşın, mantıksal isim linkini göstermesi daha kullanışlıdır. Gerekli sembolik bağlantıyı aşağıdaki gibi oluşturup, derleme zamanında kütüphanenin gerçek adı yerine kullanabiliriz.

$  ln -s libtest.so.1 libtest.so
$  gcc -odriver driver.c libtest.so.1.0.0
$  gcc -odriver driver.c -L. -ltest
$  LD_LIBRARY_PATH=. ./driver
İyileştirilmiş versiyon
8

Örnek kütüphanemiz için isimlendirmeye ilişkin durumu özetleyecek olursak:

İsimlendir Dosya Adı
Gerçek İsim (Real Name) libtest.so.1.0.0
Mantıksal İsim (Soname) libtest.so.1
Bağlayıcı İsmi (Linker Name) libtest.so

İlgili dosyalar ise aşağıdaki gibidir.

$ ls -l libtest.so* | awk '{print $9, $10, $11}'
libtest.so -> libtest.so.1
libtest.so.1 -> libtest.so.1.0.1
libtest.so.1.0.0
libtest.so.1.0.1

Bu yöntemde, uygulamayı yeniden oluşturmak zorunda kalmamamıza karşın kütüphaneye olan sembolik bağlantıyı yönetmek zorundayız. Yeni bir uyumlu versiyon yüklendiğinde soname bağlantısı yeni kütüphaneyi göstermeli, yeni bir major versiyon yüklendiğinde ise gerekli soname bağlantısı oluşturulmalı. Bu işlemler için daha önce de bahsettiğimiz ldconfig aracını kullanabiliriz. Örneğimiz üzerinden bu durumu inceleyelim.

Kütüphanemizin ilk versiyonunu, libtest.so.1.0.0, /usr/lib altına kopyalayım ve ardından ldconfig aracını çalıştırıp neler olduğuna bakalım.

# cp libtest.so.1.0.0 /usr/lib
# ldconfig -v | grep libtest
...
    libtest.so.1 -> libtest.so.1.0.0 (changed)

ldconfig tarafından /usr/lib altında soname bağlantısının oluşturulduğunu ve /etc/ld.so.cache dosyasına kütüphanemizle ilgili bir girişin eklendiğini görüyoruz.

$ ls -l /usr/lib/libtest.so*
lrwxrwxrwx 1 root root   16 Mar 15 18:42 /usr/lib/libtest.so.1 -> libtest.so.1.0.0
-rwxr-xr-x 1 root root 7848 Mar 15 18:42 /usr/lib/libtest.so.1.0.0
$ ldconfig -p /etc/ld.so.cache | grep libtest
    libtest.so.1 (libc6,x86-64) => /usr/lib/libtest.so.1

Bu durumda uygulamamızı, LD_LIBRARY_PATH değişkenini kullanmadan, çalıştırıdığımızda kütüphanenin eski versiyonuyla çalışacaktır.

$ ./driver
8

Şimdi kütüphanenin yeni versiyonunun yine /usr/lib altına atalım ve ldconfig uygulamasını çalıştıralım.

# cp libtest.so.1.0.1 /usr/lib
# ldconfig -v | grep libtest

    libtest.so.1 -> libtest.so.1.0.1 (changed)

soname bağlantısının bu kez yeni versiyonu gösterdiğini görüyoruz.

$ ls -l /usr/lib/libtest.so*
lrwxrwxrwx 1 root root   16 Mar 15 18:47 /usr/lib/libtest.so.1 -> libtest.so.1.0.1
-rwxr-xr-x 1 root root 7848 Mar 15 18:42 /usr/lib/libtest.so.1.0.0
-rwxr-xr-x 1 root root 7992 Mar 15 18:47 /usr/lib/libtest.so.1.0.1

Uygulamayı çalıştırdığımızda kütüphanenin yeni versiyonunun kullanıldığını görmekteyiz.

$ ./driver
İyileştirilmiş versiyon
8

Sembollere Versiyon Atanması (Symbol Versioning)

Sembollere versiyon atayarak, aynı kütüphane içinde bir fonksiyonun birden çok versiyonunun barındırılması hedeflenmektedir. Kütüphane içinde uyumluluğu bozacak değişiklikler yapılmasına karşın, uygulamalar kütüphane içinde bağlandıkları fonksiyonları kullanmaya devam edebilmektedir. Bu sayede bir önceki yöntemde gördüğümüz gibi kütüphanenin yeni versiyonlarını çıkarmaya gerek kalmamaktadır.

Bu amaçla, bağlayıcı versiyon betikleri (Linker Version Scripts) kullanılmaktadır. Versiyon betikleri statik bağlayıcı, ld, tarafından kullanılan, genellikle .map uzantılı, yazı dosyalarıdır. Temel olarak süslü parantezlerle gruplanmış ve başında versiyon etiketi olan düğümlerden oluşmaktadır. Basit bir örneği aşağıdaki gibidir.

VER_1 {
    global: foo;
    local: *; # Diğer semboller gizlenmekte
};

global ve local anahtar kelimeleri kütüphane içinde sembollerin görünürlüğünü belirlemektedir. local olarak belirtilen semboller kütüphane dışından kullanılamamaktadır. *, açık bir şekilde global olarak belirtilen sembollerin dışındaki tüm sembolleri göstermektedir.

Bir önceki örneğimizi bu kez versiyon betiği kullanarak yapalım. İlk olarak, kütüphanemizin ilk halini yukarıda gösterdiğimiz örnek versiyon betiğini kullanarak oluşturalım. Versiyon betiğini ver1.map olarak isimlendirebiliriz.

$ gcc -fPIC -shared -olibtest.so test.c -Wl,--version-script,ver1.map

Kütüphane içindeki foo ile ilgili semboller aşağıdaki gibidir.

$ nm libtest.so | grep foo
0000000000000630 T foo

$ readelf -s libtest.so | grep foo
     8: 0000000000000630    21 FUNC    GLOBAL DEFAULT   12 foo@@VER_1
    50: 0000000000000630    21 FUNC    GLOBAL DEFAULT   12 foo

nm çıktısında yalnız foo sembolünü görmemize karşın, readelf ile bir de foo@@VER_1 sembolünün bulunduğunu görmekteyiz. Paylaşımlı kütüphaneler ve bu kütüphanelere bağımlılığı olan çalışabilir dosyalar içinde .symtab ve .dynsym olmak üzere 2 tane sembol tablosu tutulmaktadır. .symtab kütüphane içinde tüm sembolleri içermekte ve statik bağlanım sırasında kullanılmaktadır. .dynsym ise yalnız global sembolleri içeren daha küçük bir tablodur ve dinamik bağlayıcı tarafından kullanılmaktadır. readelf çıktısındaki foo@@VER_1 sembolü .dynsym tablosunda bulunmaktadır. Normalde @ karakterinin sembol isimlerinde kullanılmasına izin verilmez. Bu sayede bu sembolün versiyonu olduğunu anlaşılmaktadır.

Şimdi uygulamamızı libtest.so ile, ara dosyaları da saklayacak şekilde, derleyelim.

$ gcc -odriver driver.c -L. -ltest --save-temps

Amaç koda baktığımızda foo sembolünün beklediğimiz isimde saklandığını görmekteyiz.

$ readelf -s driver.o | grep foo
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND foo

Uygulama içinde ise foo sembolünün sırasıyla .dynsym ve .symtab bölümlerinde versiyon bilgisiyle beraber oluşturulduğunu görüyoruz. Bağlayıcı uygulama içinde çağrılan foo sembolünün tanımını ararken kütüphane içinde foo@@VER_1 sembolüne ulaşmış ve amaç kod içindeki .dynsym tablosundaki foo sembolünü foo@VER_1 olarak değiştirmiş. Sembollerdeki @@ sembolün versiyonunun diğerlerine tercih edilmesini sağlamaktadır. Neden hem @@ hem de @ karakterlerinin kullanıldığını birden çok versiyon olduğunda daha iyi anlayacağız.

$ readelf -s driver | grep foo
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foo@VER_1 (3)
    61: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foo@@VER_1

Uygulamanın beklediğimiz gibi çalıştığını görmekteyiz.

$ LD_LIBRARY_PATH=. ./driver
8

Şimdi foo fonksiyonunun eski halini koruyarak yeni bir versiyonunu nasıl ekleyebileceğimize örnek üzerinden bakalım. Ayrıca kütüphaneye bar isimli yeni bir fonksiyon da ekledik. Kütüphane kodunun yeni hali aşağıdaki gibidir.

#include <stdio.h>

__asm__(".symver foo_old,foo@VER_1");
__asm__(".symver foo_new,foo@@VER_2");

int foo_old(int val) {
    return val / 2;
}

int foo_new(int val) {
    puts("İyileştirilmiş versiyon");
    return val >> 1;
}

void bar() {
    printf("v2 bar\n");
}

Kütüphane içinde sembolik makina direktifleri görmekteyiz. .symver ile bir sembolün başka isimle bir kopyası (alias) oluşturulur. Bu takma isim normalde izin verilmeyen @ karakterini içerebilmektedir. Genel şekli aşağıdaki gibidir.

.symver name, name2@nodename

name2 sembolün gerçek adını, nodename ise versiyon bilgisini göstermektedir. Kütüphanenin bir önceki versiyonunda uygulamamanın foo@VER_1 sembolüne bağlandığını hatırlayınız. Bu sembol şimdi foo_old fonksiyonunu göstermektedir. Kütüphanenin yeni halini kullanacak uygulamalar ise foo@@VER_2 sembolünü dolayısıyla foo_new fonksiyonunu kullanacaklardır. @@ karakterleri versiyonun öncelikli olduğunu göstermektedir. Bu aşamada versiyon betiğine VER_2 düğümünü eklemeliyiz. Aşağıdaki versiyon betiğini ver2.map adıyla saklayıp kullanabiliriz. VER_2 düğümünün sonundaki VER_1, iki versiyon arasında ilişki kurmakta ve ilk versiyondaki global ve local bildirimlerinin ikinci versiyonda da geçerli olmasını sağlamaktadır.

VER_1 {
    global: foo;
    local: *;
};

VER_2 {
    global: bar;
} VER_1;

Kütüphaneyi yeniden derleyelim ve foo'ya ilişkin sembollere bakalım.

$ gcc -fPIC -shared -olibtest.so test.c -Wl,--version-script,ver2.map
$ readelf -s libtest.so | grep foo
     9: 0000000000000710    21 FUNC    GLOBAL DEFAULT   12 foo@VER_1
    10: 0000000000000725    30 FUNC    GLOBAL DEFAULT   12 foo@@VER_2
    40: 0000000000000725    30 FUNC    LOCAL  DEFAULT   12 foo_new
    44: 0000000000000710    21 FUNC    LOCAL  DEFAULT   12 foo_old
    ...

foo_old ve foo_new sembollerinin LOCAL yani dışsal bağlanıma kapalı olduğunu, bunun yerine GLOBAL düzeydeki takma isimlerinin kullanıldığını görüyoruz.

Uygulamamıza bu kez driver2 ismini vererek, kütüphanenin bu haliyle derleyelim ve foo çağrısının hangi sembol ile temsil edildiğine bakalım.

gcc -odriver2 driver.c -L. -ltest --save-temps
$ readelf -s driver | grep foo
    3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND foo@VER_2 (3)
    ...

Uygulamamızı çalıştırdığımızda foo fonksiyonunun yeni versiyonunun kullanıldığını görmekteyiz.

$ LD_LIBRARY_PATH=. ./driver2
İyileştirilmiş versiyon
8

Kütüphanenin eski versiyonuna bağımlı uygulamamız da beklediğimiz gibi çalışmakta.

$ LD_LIBRARY_PATH=. ./driver
8

Sembol versiyonları kullanılarak, kütüphane kodunda kapsamlı değişlikler yapılmasına karşın, tek bir kütüphane dosyası ile tüm versiyonlar kullanılabilmektedir. glibc 2.1 versiyonundan beri kütüphane içinde versiyon sembolleri kullanılmakta ve tek bir major kütüphane dosyası, libc.so.6, bulunmaktadır.

results matching ""

    No results matching ""