Dinamik Yükleme

Dinamik bağımlılığı olan uygulamalar ihtiyaç duydukları tüm kodu bünyelerinde bulundurmamaktadırlar. Bu tür uygulamalar yüklenirken, akış program koduna geçmeden önce, ihtiyaç duydukları kodları barındıran paylaşımlı kütüphaneler yüklenmiş olmalıdır. Gerekli kütüphanelerin bulunması ve yüklenmesi dinamik bağlayıcının görevidir. Derleme aşamasında bağlayıcı, uygulamanın ihtiyaç duyduğu paylaşımlı kütüphanelerin isimlerini çalışabilir dosya içine not etmektedir. Bu sayede dinamik bağlayıcı gerekli kütüphane dosyalarını tespit edebilmektedir.

Not: Kütüphanelerin, uygulama kodu tarafından yüklendiği çalışma şekli bu durumun dışındadır. Bu durumu daha sonra inceleyeceğiz.

Basit bir örnek üzerinden bu durumu inceleyelim. Uygulama ve kütüphane koduna driver.c ve test.c adını verelim.

void foo();

int main() {
    foo();
    return 0;
}
void foo() {

}

Kütüphane ve çalışabilir dosyayı aşağıdaki gibi oluşturabiliriz.

$ gcc -fPIC -shared -olibtest.so test.c
$ gcc -odriver driver.c libtest.so

Uygulama oluşturulurken bağlayıcı gerekli kütüphane isimlerini ELF formatında DT_NEEDED etiketine yazmaktadır. readelf aracı ile uygulamanın bağımlı olduğu kütüphane listesine ulaşılabiliriz. Örneğimiz için bağımlılık listesi aşağıdaki gibidir.

readelf --dynamic driver | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libtest.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Bağımlılık listesinde libtest.so ile beraber, gcc tarafından bağlayıcıya gizli bir şekilde geçirilen, stadart kütüphane dosyasını da (libc.so.6) görmekteyiz.

Bağlayıcı yalnız gerekli dosyaları uygulamanın bağımlılık listesine eklemektedir. Daha önce statik kütüphaneleri incelediğimiz bölümde de söylediğimiz gibi, bağlayıcı bir dosya içinde tanımı bulunmayan bir sembolle karşılaştığında içsel bir tabloya bu sembolü kaydetmekte ve sonraki dosyalarda bu sembolün tanımını aramaktadır. Örneğimiz için bağlayıcı driver.c dosyasından üretilen amaç kod içinde foo fonksiyonunun tanımını bulamamış, daha sonra bu tanımı libtest.so dosyasında bularak kütüphane dosyasının ismini çalışabilir dosyanın bağımlılık listesine eklemiştir.

Paylaşımlı kütüphanelerden uygulamaya kod kopyalanmadığından, ilk bakışta paylaşımlı kütüphanelere yalnız çalışma zamananında ihtiyaç olduğu düşünülebilir. Fakat bahsettiğimiz nedenden dolayı bağlanma aşamasında da paylaşımlı kütüphane dosyalarına ihtiyaç duyulmaktadır, kütüphane dosyalarının sembol tabloları bağlayıcı tarafından kullanılmaktadır.

Not: Windows altında ise, dll dosyaları içindeki sembol bilgilerini barındıran ayrı .lib uzantılı dosyalar oluşturulmakta ve bağlanma işleminde bu dosyalar kullanılmaktadır.

Şimdi bağlayıcının gerekli kütüphaneleri belirlerken izlediği yola bir önceki örneğimizi genişleterek tekrar bakalım.

void foo();

void foo() {
    puts("FOO");
}

int main() {
    foo();
    return 0;
}
void foo() {
    puts(__func__);
}

void bar() {
    puts(__func__);
}

Gerekli dosyaları bir önceki örnekteki gibi yeniden oluşturalım.

$ gcc -fPIC -shared -olibtest.so test.c
$ gcc -odriver driver.c libtest.so

Uygulamanın bağımlılık listesinde bu sefer libtest.so dosyası bulunmamaktadır.

$ readelf --dynamic driver | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Bağlayıcı foo tanımını bulduğu için libtest.so dosyasının gerekli olmadığı sonucuna varmıştır. Böyle bir durumda bağlayıcıya --no-as-needed seçeneği geçirilerek, komut satırındaki tüm kütüphanelerin uygulamanın bağımlılık listesine eklenmesi sağlanabilir.

$ gcc -odriver driver.c -Wl,--no-as-needed libtest.so

$ readelf --dynamic driver | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libtest.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

Bu durumda hangi foo fonksiyonunun çağrılacağını merak edebilirsiniz, bu konuya daha sonra değineceğiz fakat bu noktada uygulama içindeki kodun çağrılacağını söyleyelim. Kullanılmıyor gibi gözüken bir kütüphanenin neden yüklenmek istenebileceğine de yine daha sonra değineceğiz.

Gerekli kütüphaneler dinamik bağlayıcı tarafından yüklendikten sonra uygulama başlatılmaktadır. Uygulamayı başlatan işletim sistemi yükleyicisi değil, dinamik bağlayıcının kendisidir. Dinamik bağımlılığı olan bir uygulamanın derlemesinden çalıştırılmasına kadar geçen süreci ve görev alan araçları kabaca aşağıdaki gibi gösterebiliriz.

Örneğimiz üzerinden gidecek olursak driver uygulamasını çalıştırmak istediğimizde ilk olarak kabuk (shell) tarafından işletim sistemi yükleyicisi çağrılacaktır. Uygulamanın dinamik bağımlılıkları olması durumunda yükleyici dinamik bağlayıcıyı çağıracak, dinamik bağlayıcı da gerekli kütüphaneleri yükledikten sonra uygulamayı çalıştıracaktır. İncelemelerimizin başından beri sürekli bağlayıcı ve dinamik bağlayıcıdan bahsettik. Şimdi bu araçlara daha yakından bakalım.

Statik ve Dinamik Bağlayıcı

İlk olarak bu bağlayıcıların isimlendirilmesinden bahsedelim. Bağlayıcılardan bir tanesi derleme diğeri ise çalışma zamanında görev almaktadır.

Derleme ya da statik zaman (compile time, static time) olarak adlandırılan süreçte kullanılan bağlayıcı, kısaca bağlayıcı ya da statik bağlayıcı olarak isimlendirilebilir. İngilizce linker, static linker ve link editor isimlerinin kullanıldığını görmekteyiz.

Çalışma zamanında görev alan bağlayıcıyı ise dinamik bağlayıcı olarak kullandık. İngilizce dynamic linker, run-time linker ve dynamic linking loader terimlerinin kullanıldığını görmekteyiz.

Statik bağlama işlemi, binutils paketinden çıkan ld uygulaması tarafından sağlanmaktadır. gcc, ld uygulamasını uygun parametre ve seçeneklerle gizli olarak çağırmaktadır. Dinamik bağlama işlemi ise kendisi de paylaşımlı kütüphane olan çalıştırılabilir bir dosya tarafından sağlanmaktadır. Bir paylaşımlı kütüphanenin bir uygulamaya bağlanmaksızın kendi başına çalıştırılabilir olması ilk bakışta tuhaf gelebilir. Bu durumu ilerleyen kısımda inceleyeceğiz. Dinamik bağlayıcı libc paketinden çıkmaktadır. Kullandığımız sistemde, 32 ve 64 bitlik dinamik yükleyiciler ve paketleri aşağıdaki gibidir.

Paket Dinamik Yükleyici
libc6:i386 /lib/ld-linux.so.2
libc6:amd64 /lib64/ld-linux-x86-64.so.2

Dinamik bağlayıcının dışsal bir bağımlılığı bulunmamaktadır ve işletim sistemi yükleyicisi tarafından yüklenip çalıştırılabilmektedir. İşletim sistemi, yükleyeceği dinamik bağlayıcıyı çalışabilir dosya içeriğinden öğrenmektedir. Derleme zamanında statik bağlayıcı ELF içinde .interp (interpreter) alanına kullanılması istenen dinamik bağlayıcının yol ifadesini yazmaktadır. readelf ile bu değere aşağıdaki gibi ulaşabiliriz.

$ readelf -l driver | grep interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

Dinamik bağlayıcıyı çalıştırdığımızda aldığı seçenekleri gösteren bir yardım mesajıyla karşılaşmaktayız. Bu zamana kadar bir uygulamanın dinamik bağımlılıklarını ELF içindeki ilgili alana bakarak listeledik. ELF içindeki dinamik bağımlılık listesi, uygulamanın direkt bağımlı olduğu kütüphaneleri listelemektedir. Buradaki kütüphanelerin de başkaca bağımlılıkları olabilir. Dinamik bağlayıcı bu bağımlılıkları da çözmektedir. Aşağıdaki çıktıları inceleyiniz.

$ readelf --dynamic driver | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libtest.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
$ LD_TRACE_LOADED_OBJECTS=1 /lib64/ld-linux-x86-64.so.2 ./driver
    linux-vdso.so.1 =>  (0x00007fff96d5b000)
    libtest.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f0b5c8d0000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f0b5ccba000)

Dinamik bağlayıcınının davranışını değiştiren çevre değişkenlerinden yeri geldiğince bahsetmeye çalışacağız. Dinamik bağlayıcının başındaki çevre değişkeninin kullanılmaması durumunda bağlayıcı libtest.so dosyasını bulamadığı için hata vererek sonlanacaktır. Dinamik bağlayıcının kütüphaneleri arama kurallarına daha sonra bakacağız. Uygulamaların dinamik bağımlılıklarını izlemek için Linux sistemlerinde ayrıca ldd isimli, libc-bin paketinden çıkan bir betik (script) dosyası da mevcuttur. Dinamik bağlayıcıyı açık bir şekilde kullanmaksızın bir uygulamanın tüm dinamik bağımlılıklarını aşağıdaki gibi de daha kolay bir şekilde listeleyebilirsiniz.

$ ldd driver
    linux-vdso.so.1 =>  (0x00007fffee5fc000)
    libtest.so => not found
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2e5dcd5000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f2e5e0bf000)

Bu bölümde son olarak paylaşımlı bir kütüphanenin nasıl çalıştırılabileceğinden kısaca söz edelim.

Çalıştırılabilir Paylaşımlı Kütüphaneler

Bir kütüphane dosyasının kendi başına çalıştırılabilir olması, dinamik bağlayıcı dışında çok aranan bir özellik değildir. Buna karşın, kütüphanenin kullanımı, versiyonu ve sahipliği hakkında bilgi vermek için kullanılabilir. Örneğin standart C kütüphanesi böyle bir kullanıma sahiptir.

$ /lib/x86_64-linux-gnu/libc.so.6

GNU C Library (Ubuntu GLIBC 2.19-10ubuntu2.1) stable release version 2.19, by Roland McGrath et al.
Copyright (C) 2014 Free Software Foundation, Inc.
...

Bir uygulamanın hangi noktadan itibaren çalışmaya başlayacağı (entry point) bağlayıcı tarafından çalıştırılabilir dosya içine yazılmaktadır. İşletim sistemi yükleyicisi veya dinamik bağlayıcı, ELF içindeki bu adrese dallanarak kontrolü uygulama koduna bırakmaktadır. Bağlayıcı çalıştırılabilir dosyalar için öngörülen giriş noktası olarak, tanımı derleyicinin başlangıç (startup) kodlarında bulunan ve nihayetinde main fonksiyonunu çağıracak olan, _start sembolünü seçmektedir. readelf --file-header şeklinde bu adrese ulaşabilirsiniz. Bağlayıcı ayrıca giriş noktasının kullanıcı tarafında belirlenebilmesine izin vermektedir. Aşağıdaki örneği inceleyiniz.

#include <stdio.h>
#include <stdlib.h>

void foo() {
    puts(__func__);
    exit(0);
}

int main() {
    puts(__func__);
    return 0;
}
$ gcc -otest test.c -Wl,--entry=foo

test uygulamasını çalıştırdığımızda main yerine foo fonksiyonunun çalıştığını görmekteyiz.

$ ./test
foo

Paylaşımlı dosyaların dinamik bağlayıcı tarafından yüklendiğini ve dinamik bağımlılığı olan uygulamaların yine dinamik bağlayıcı tarafından çalıştırılığını söylemiştik. Bu durumda paylaşımlı dosyamızın .interp alanına dinamik yükleyiciyi yazmalı ve istediğimiz giriş noktasını belirtmeliyiz. .interp alanı çalıştırılabilir dosyalar için statik bağlayıcı tarafından doldurulmaktadır, paylaşımlı kütüphaneler için ise bu değeri biz yazmalıyız. Aşağıdaki örneği inceleyiniz.

#include <stdio.h>
#include <stdlib.h>

#ifdef __x86_64__
const char interp[] __attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
#else
const char interp[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";
#endif

void foo() {

}

void version() {
    puts("Version 1.9.7.8");
    exit(0);
}

Uygulamanın 32 veya 64 bit hedefli derlenmesi durumunda uygun dinamik bağlayıcı ifadesi öntanımlı __x86_64__ makrosu ile seçilmekte ve GNU C eklentisiyle .interp alanına yazılmaktadır. Kütüphanemizi aşağıdaki gibi oluşturup çalıştırabiliriz.

$ gcc -fPIC -shared -olibtest.so test.c -Wl,--entry=version
$ ./libtest.so
Version 1.9.7.8

results matching ""

    No results matching ""