Statik Kütüphaneler

Statik kütüphaneleri incelemeden önce bir C kodunun çalıştırılabilir hale gelene kadar geçtiği aşamalara kısaca bakalım.

test.c adıyla sakladığımız basit bir örneği, gcc uygulamasına --save-temps anahtarını geçirerek, aşağıdaki gibi derleyebiliriz.

int main() {
    return 0;
}
$ gcc -otest test.c --save-temps

--save-temps anahtarı ile derleyicinin ürettiği ara kodlar uygun ad ve uzantılarla dosya sistemine kaydedilmektedir. test.c için derleyici aşağıdaki dosyaları üretecektir.

Dosya Adı İçerik
test.i Önişlemcinin ürettiği kod
test.s Derleyicinin ürettiği sembolik makina kodları
test.o Gerçek makina kodlarını içeren ELF formatlı amaç kod
test Çalıştırılabilir ELF formatlı kod

Bir C kodu çalıştırılabilir hale gelene kadar, temel olarak, aşağıdaki aşamalardan geçmektedir.

  • Önişlem aşaması
  • Derleyeci tarafından sembolik makina kodlarının üretilmesi
  • Assembler tarafından gerçek makina kodlarının üretilmesi
  • Bağlayıcı tarafından çalıştırılan dosyanın üretilmesi

Not: Komut satırından kullandığımız gcc uygulaması aslında derleyici değil, derleme sürecinde gerekli olan uygulamaları uygun sıra ve parametrelerle çağıran bir sürücü (driver) programdır.

Fakat çoğu zaman detaya girmeden bütün süreçten derleme işlemi olarak bahsedeceğiz.

Bağlayıcı .o uzantılı amaç dosyaları (object file) birleştirerek nihai çalıştırılabilir dosyayı oluşturmaktadır. Bu durumda ihtiyaç duyulacak tüm fonksiyonları tek bir amaç dosyada toplamak mümkündür. Bir önceki örnek kodumuza foo fonksiyonu çağrısını eklediğimizi ve foo ile beraber diğer tüm fonksiyon tanımlarının liball.o amaç dosyasında olduğunu varsayalım.

void foo();

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

Kodu aşağıdaki gibi derleyebiliriz.

$ gcc -otest test.c liball.o

Bu yöntemde bir problemle karşılaşmaktayız, bağlayıcı tarafından, liball.o içeriğinin tamamı çalışabilir dosyaya kopyalanacak ve test uygulaması gereksiz yere büyüyecektir. Diğer bir alternatif ise birbiriyle ilişkili olduğu düşünülen fonksiyonları aynı amaç dosyada toplamak olabilir. Tek bir liball.o yerine lib1.o, lib2.o,..., libN.o dosyalarına sahip olduğumuzu ve foo tanımının lib1.o içinde olduğunu kabul edelim. Bu durumda daha küçük bir çalışabilir kod elde etmek mümkündür.

$ gcc -otest test.c lib1.o

Fakat bu yöntemde de çok sayıda amaç dosyayı yönetmek zorlaşacaktır. Hangi fonksiyonun hangi amaç dosya içinde olduğu bilinmeli ve çoğu durumda komut satırına birden çok amaç dosya geçirilmelidir.

Statik kütüphaneler bu problemleri gidermek için geliştirilmiştir. Birbiriyle ilişkili olan fonksiyonları içeren amaç dosyalar tek bir dosya halinde arşivlenerek saklanmaktadır. Statik kütüphane içinden yalnız gerekli amaç dosyalar çalıştırılabilir koda kopyalanmaktadır. Statik kütüphane oluşuturmak için Linux altında ar aracı kullanılmaktadır. Örnek bir statik kütüphane aşağıdaki gibi oluşturulabilir.

$ ar rcs liball.a lib1.o lib2.o ... libN.o

Kütüphane dosyalarına, bir zorunluluk olmamasına karşın geleneksel olarak, .a (archive) uzantısı verilmektedir. Şimdi statik kütüphane kullanımı göstermek için basit bir örnek yapalım.

Örnek Kütüphane Kullanımı

Aşağıdaki örnek kodları sırasıyla test.c, foo.c ve bar.c isimleriyle saklayalım.

void foo();

int main() {
    foo();
    return 0;
}
#include <stdio.h>

void foo() {
    puts(__func__);
}
#include <stdio.h>

void bar() {
    puts(__func__);
}

foo.c ve bar.c dosyalarını kullanarak statik bir kütüphaneyi aşağıdaki gibi oluşturabiliriz. gcc uygulamasına geçirilen -c (compile) anahtarı, amaç dosyalar oluşturulduktan sonra bağlayıcının çağrılmasını engelemektedir.

gcc -c foo.c
gcc -c bar.c
ar rcs liball.a foo.o bar.o

test.c adıyla sakladığımız uygulamamız, foo fonksiyonun tanımına gereksinim duymaktadır. Kütüphane dosyamızı gcc uygulamasına geçirerek uygulamamızı derleyebilir ve çalıştırabiliriz. Bu örnek için kütüphane dosyasının çalıştığımız dizinde olduğunu varsayıyoruz.

$ gcc -otest test.c liball.a

$ ./test
foo

Daha önce kütüphane içinden yalnız gerekli modüllerin çalışabilir dosyaya kopyalandığını söylemiştik. Bu durumda yalnız, foo fonksiyonunun tanımının bulunduğu, foo.o dosyasının test uygulamasına kopyalanmasını bekliyoruz. test uygulaması içindeki sembollere bakarak bu durumu gözleyebiliriz. Bu amaçla nm uygulamasını kullanabiliriz.

$ nm test | grep foo
000000000040054b T foo

$ nm test | grep bar

nm çıktısındaki T (Text) harfi, fonksiyon tanımının dosya içinde bulunduğunu göstermektedir. Uygulama içinde bar fonksiyonuna ilişkin herhangi bir sembol dolayısıyla taşınmış kod bulunmamaktadır.

Burada bir noktaya dikkatinizi çekmek istiyoruz. Statik kütüphanelerin komut satırındaki sıralaması önem taşımaktadır. Aynı örneği şimdi kütüphane dosyasını uygulama kodundan daha önce geçirerek derleyelim. Bu durumda aşağıdaki gibi bir hata ile karşılaşmaktayız.

$ gcc -otest liball.a test.c
/tmp/ccjg4071.o: In function `main':
test.c:(.text+0xa): undefined reference to `foo'
collect2: error: ld returned 1 exit status

foo fonksiyonunun tanımı liball.a içinde bulunmasına karşın, bağlayıcı bize foo fonksiyonunun tanımını bulamadığından şikayet etmekte. Bağlayıcı komut satırında gösterilen dosyaları sırasıyla okumakta ve tanımını bulamadığı referansları, daha sonra çözümlemek üzere, kaydetmektedir. Bağlayıcı tanımsız bir referans gördüğünde bunu daha sonra gördüğü dosyalarda aramakta, geriye doğru bir arama yapmamaktadır. Hata aldığımız örnek için bağlayıcı foo çağrısıyla karşılaştığında, kütüphane dosyası üzerinden zaten geçtiği için foo fonksiyonunun tanımını bulamamaktadır. Böyle bir durumda bağlayıcı istenilen sembolleri aramaya zorlanabilir. undefined anahtarı ile tanımı aranacak referansı açık bir şekilde geçirebiliriz.

$ gcc liball.a -otest test.c -Wl,--undefined=foo

$ ./test
foo

Not: gcc'ye geçirilen Wl anahtarı devam eden anahtarların bağlayıcıya geçirileceğini göstermektedir.

results matching ""

    No results matching ""