Linux Yazılım Notları

Kütüphanelerin Erişime Kapalı Alanları

Uygulamalar, kütüphanelerin içsel alanlarına direkt olarak olarak erişememelerine karşın bu alandaki değişimden, istenmeyen şekilde, etkilenmektedir. Bir örnek üzerinden bu durumu inceleyelim.

İçerisinde Class isimli bir sınıf tanımladığımız kütüphanenin başlık ve kaynak dosyaları aşağıdaki gibi olsun.

test.h:

class Class {
public:
    Class();
    int foo();
private:
    int _foo;
    void init();
};

test.cpp:

#include "test.h"

Class::Class() {
    init();
}

int Class::foo() {
    return _foo;
}

void Class::init() {
    _foo = 111;
}

Gizli _foo değişkenine yine gizli init fonksiyonu ile ilk değeri verilmekte, değeri ise dışsal erişime açık foo fonksiyonu üzerinden öğrenilmektedir.

Kütüphane dosyasını, libtest.so.1 ismiyle, aşağıdaki gibi derleyelim.

$ g++ -fPIC -shared -olibtest.so.1 test.cpp

Kütüphaneyi kullanacak basit bir uygulama kodunu ise aşağıdaki gibi tanımlayıp derleyebiliriz.

app.cpp:

#include <iostream>
#include "test.h"

using namespace std;

int main() {
    Class obj;
    cout << obj.foo() << endl;
    return 0;
}
$ ln -s libtest.so.1 libtest.so
$ g++ -oapp app.cpp -L. -ltest

Derleme sürecini kolaylaştırmak için, kütüphanenin versiyon numarası içermeyen sembolik bir bağlantısını oluşturduğumuza dikkat ediniz.

Uygulamayı aşağıdaki gibi çalıştırabiliriz.

$ LD_LIBRARY_PATH=. ./app
111

Şimdi kütüphanenin sağladığı foo fonksiyonunu iyileştirmek istediğimizi düşünelim, bu durumda kütüphanenin, erişilebilir arayüzüne dokunmaksızın, eskisiyle uyumlu yeni bir versiyonunu çıkarmak isteyebiliriz. foo fonksiyonunun yeni alanlara ihtiyaç duyduğunu varsayalım, bu durumu temsil etmek için kütüphanenin içsel alanına int türden 100 elemanlı bir dizi daha ekleyip ilklendirelim. Kütüphanenin yeni kodu aşağıdaki gibi olacaktır.

class Class {
public:
    Class();
    int foo();
private:
    int _foo;
    int _bar[100];
    void init();
};
#include "test.h"

Class::Class() {
    init();
}

int Class::foo() {
    return _foo;
}

void Class::init() {
    _foo = 111;
    for (int i = 0; i < 100; ++i) {
        _bar[i] = 0;
    }
}

Kütüphaneyi libtest.so.2 adıyla yeniden derleyelim ve var olan uygulamayı tekrar çalıştıralım. Bu kez sembolik bağlantımız yeni kütüphaneyi göstermeli.

$ g++ -fPIC -shared -olibtest.so.2 test.cpp
$ rm libtest.so
$ ln -s libtest.so.2 libtest.so
$ LD_LIBRARY_PATH=. ./app
111
Segmentation fault (core dumped)

Uygulamayı yeni kütüphane ile çalıştırdığımızda hata almaktayız. Tekrar derleyip çalıştırdığımızda ise hatasız çalıştığını görmekteyiz.

$ g++ -oapp app.cpp -L. -ltest
$ LD_LIBRARY_PATH=. ./app
111

Uygulamaya kütüphaneden herhangi bir kod taşınmamasına karşın, neden uygulama yeniden derlendiğinde sorun ortadan kalkmaktadır? Şimdi bu sorunun cevabını arayalım.

Bir sınıfa ait örnek (instance) oluşturulurken, ilk önce bellekte gerekli alan ayrılmakta, sonrasında bu alan üzerinde sınıfın başlangıç fonksiyonu (constructor) çalıştırılmaktadır. Örneğimiz için sınıfın private alanı sınıf örneğinin bellekteki görüntüsünü oluşturmaktadır.

Uygulama derlenirken, derleyici tarafından sınıfın başlık dosyasındaki bildirimine bakılmakta ve gerekli alanı tahsis edecek kod yazılmaktadır. Kütüphanenin değiştirilmeden önceki ve sonraki halleri için main fonksiyonuna ait sembolik makina kodlarının bir kısmı aşağıdaki gibidir.

main:
.LFB971:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $32, %esp
        leal    28(%esp), %eax
        movl    %eax, (%esp)
        call    _ZN5ClassC1Ev

main:
.LFB971:
        pushl   %ebp
        movl    %esp, %ebp
        andl    $-16, %esp
        subl    $432, %esp
        leal    28(%esp), %eax
        movl    %eax, (%esp)
        call    _ZN5ClassC1Ev

Not: Uygulamaya ait 32 bitlik sembolik makina kodlarını görmek için uygulamayı aşağıdaki gibi derleyip app.s dosyasını inceleyebilirsiniz. Benzer işlemi, m32 anahtarını kaldırarak, 64 bit için de yapabilirsiniz.

$ g++ -oapp app.cpp -L. -ltest -m32 --save-temps

Sembolik makina kodlarındaki assembler direktiflerini ise, sadeleştirme amacıyla, ihmal ediyoruz.

İlk durumda, derleyicinin main için yığında (stack) 32 byte yer ayırdığını, ikinci durumda ise 432 byte yer ayırdığını görmekteyiz.

Uygulama, kütüphaneye ilişkin yeni başlık dosyası kullanılarak, ikinci kez derlenirken, private alana yeni eklenen int _bar[100] dizisi için sizeof(int) * 100 yani fazladan 400 byte daha yer ayrılmıştır.

Uygulamanın yeniden derlenmeksizin kütüphanenin yeni versiyonuyla çalıştırıldığında neden erişim hatası (Segmentation fault) aldığını merak edebilirsiniz.

Fonksiyonların geri dönüş değerleri yığında saklanmaktadır, uygulama kodunda 32 byte yer ayrılmasına karşın kütüphane kodu bu alandan başlayarak fazladan 400 byte uzunluğunda bir alan üzerinde işlem yapmakta ve örneğimiz için bu alanı 0 değeriyle doldurmaktadır. Bu durumda main geri dönüş adresi 0. adres olacak ve işlemci bu adrese dallandığında erişim hatası oluşacaktır.

Oluşan hata durumunu görsel olarak aşağıdaki gibi temsil edebiliriz. İlk bellek görünümü, uygulamanın kütüphanenin ilk versiyonuna linklenmiş olduğu durumu sonraki ise kütüphanenin ikinci versiyonuna linklenmesi durumunu temsil etmektedir.

Not: Uygulama içinde, sınıf örneği yığında değilde dinamik olarak heap alanında oluşturulsaydı da benzer bir problemin oluşabileceğine dikkat ediniz. Sınıfın başlangıç fonksiyonu bu kez heap alanında kendisi için ayrılan alanın dışında işlem yapacaktı.

Yukarıda incelediğimiz probleme ek olarak, kütüphanenin içsel alanındaki değişkenlerin sıralamasının değişmesi de tespit edilmesi zor olan bir probleme neden olabilmektedir. Bir örnek üzerinden bu durumu inceleyelim.

Küçük değişiklikler yaptığımız kütüphane dosyalarımız ve uygulama dosyamız aşağıdaki gibi olsun.

test.h:

class Class {
public:
    Class();
    int foo() { return _foo; }
private:
    int _foo;
    int _bar;
    void init();
};

test.cpp:

#include "test.h"

Class::Class() {
    init();
}

void Class::init() {
    _foo = 111;
    _bar = -1;
}

app.cpp:

#include <iostream>
#include "test.h"

using namespace std;

int main() {
    Class obj;
    cout << obj.foo() << endl;
    return 0;
}

Sırasıyla kütüphane ve uygulama dosyalarımızı daha önce yaptığımız gibi derleyelim ve çalıştıralım.

$ g++ -fPIC -shared -olibtest.so test.cpp
$ ln -s libtest.so.1 libtest.so
$ g++ -oapp app.cpp -L. -ltest

$ LD_LIBRARY_PATH=. ./app
111

Beklediğimiz üzere kütüphane içinde tanımlı _foo değerini doğru bir şekilde okuduk.

Kütüphanenin private alınında değişiklik yaparken bir nedenden dolayı değişkenlerin sıralamasını değiştirdiğimizi düşünelim. Bu durumda başlık dosyamız aşağıdaki gibi olacaktır.

test.h

class Class {
public:
    Class();
    int foo() { return _foo; }
private:
    int _bar;
    int _foo;
    void init();
};

Bu şekilde kütüphanenin yeni bir versiyonunu çıkaralım.

$ g++ -fPIC -shared -olibtest.so.2 test.cpp
$ rm libtest.so
$ ln -s libtest.so.2 libtest.so

Uygulamayı tekrar çalıştırdığımızda _foo değil _bar değişkeninin değerini okuduğumuzu görmekteyiz.

$ LD_LIBRARY_PATH=. ./app
-1

İlk bakışta bu tip bir hatayı beklemiyor olabilirsiniz. Kütüphaneyi, Class sınıfının private alanındaki değişimden sonra yeniden derlendiğimiz için, foo fonksiyonunun bu değişen duruma göre yeniden yazıldığını düşünebilirsiniz.

foo fonksiyonu kaynak dosya içinde tanımlansaydı durum tam da böyle olacaktı, fakat foo sınıf bildiriminin gövdesinde, yani başlık dosyasında tanımlandığı için inline olarak ele alınmaktadır (implicitly inline). Bu durumda foo kodu uygulamaya taşınmakta ve foo için kütüphane çağrısı yapılmamaktadır. Uygulama, kütüphanedeki değişimden bağımsız olarak, derlendiği andaki foo fonksiyonunu kullanmaktadır.

Uygulamaya ait sembolik makina komutlarında bu durumu gözleyebiliriz.

main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    leal    24(%esp), %eax
    movl    %eax, (%esp)
    call    _ZN5ClassC1Ev
    leal    24(%esp), %eax
    movl    %eax, (%esp)
call    _ZN5Class3fooEv
    ...
_ZN5Class3fooEv:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %eax
    movl    (%eax), %eax
    popl    %ebp
    ret

Not: c++filt ile sembolik makina listesindeki isimlerin dekore edilmemiş açık hallerini öğrenebilirsiniz.

$ c++filt _ZN5Class3fooEv

Class::foo()

Uygulamayı kütühanenin yeni haliyle yeniden derlediğimizde, uygulama kodundaki foo, fonksiyonunun değiştiğini görmekteyiz.

_ZN5Class3fooEv:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %eax
    movl    4(%eax), %eax
    popl    %ebp
    ret

Buraya kadar olan incelemelerimizi özetleyecek olursak, dinamik bağımlılığı olan uygulamaların, kütüphanelerin içsel alanlarındaki değişimlerden etkilendiğini görmekteyiz. Kütüphanelerin içsel alanlarının değişmesi durumunda uygulamalar yeniden derlenmek zorunda kalacaktır.

Böylesi bir durum kütüphane gelişimini kısıtlamaktadır. Kütüphane geliştiricileri, kütüphaneninin erişebilir arayüzüne dokunmadan, bazı özellikleri eklemek veya iyileştirmek için, kütüphanenin içsel alanlarında değişiklik yapabilmelidir. Bu sayede eskisiyle uyumlu yeni versiyonlar çıkarmak mümkün olacaktır.

Şimdi bu kısıtlamayı, d-göstericisi ile nasıl ortadan kaldırabileceğimize bakalım.