Nesne Dosyaları (Object Files) ve Statik/Dinamik Kütüphaneler
“Kaynak dosyasından binary dosyasına dönüşüm. Bir C programının derlenme aşamaları” yazı serisinde
- Preprocessing
- Compilation
- Assembly
- Linking
süreçlerinden bahsetmiştim. Hatırlarsak Assembly aşamasından sonra relocatable nesne dosyası (.o, .obj) oluşturuluyordu. Linking aşaması ile birlikte de bu relocatable nesne dosyaları aslında bir bütün haline getirilerek executable bir nesne dosyasına (.out, .exe) dönüştürülüyordu. Bu yazımda bu nesne dosyaları hakkında bilgi vereceğim. Son olarak da bu nesne dosyalarıyla bağlantılı olduğu için statik ve dinamik kütüphanelerden bahsedeceğim.
Linking aşamasından sonra birçok linker executable nesne dosyası olarak varsayılan a.out nesne dosyasını üretmektedir. a.out dosya ismi ve uzantısı assembler output’dan gelmektedir. Günümüzde artık .out yerine aşağıdaki nesne dosya formatları kullanılmaktadır.
- ELF (Linux ve birçok Unix-like işletim sistemleri)
- Mach-O (macOS ve iOS) systems
- PE (Microsost Windows)
Yazı boyunca teknik terim tekrarlarını engellemek amaçlı aşağıdaki kısaltmaları kullanacağım.
ROF : Relocatable Object File
EOF : Executable Object File
Relocatable Nesne Dosyaları (Relocatable Object Files)
ROF derleme zincirinde assembly sürecinden sonra elde edilir. Bu nesne dosyalarının içerisinde
- Makine seyiyesinde komutlar (Machine-level Instructions)
- .bss, .data ve .text segmentleri
- Tanımlanmış ve referans gösterilmiş sembollleri içeren sembol tablosu (Symbol Table)
Peki bu nesne dosyalarına relocatable (yeniden yerleştirilebilir) denilmesinin nedeni nedir?
Ana nedeni linking aşamasında linker’in elindeki birçok ROF’u bir araya getirerek birleştirilmiş bir nesne dosyası oluşturmasıdır.
Yani bir ROF içerisinde yer alan makine seviyesindeki bir komutun hemen ardına başka ROF içerisinde yer alan makine seviyesindeki bir komut EOF içerisinde yerleştirilebilir. Yani bu komutlar taşınabilir ve yeniden konumlandırılabilir. Bunun gerçekleşmesi için de komutların ROF içerisinde gerçek adres tanımlamaları yapılmaz. Gerçek adreslerini sadece linking aşamasından sonra alabilirler. Bu yüzden biz bu nesnelere relocatable diyoruz.
Yukarıda bahsettiğim mevzuyu Linux üzerinde readelf aracını kullanarak göstereceğim. Örnek olarak iki tane tamsayıyı toplayan bir fonksiyonu add.c içerisine yazıp daha sonradan main.c içerisinden çağıralım. Toplama işlemi fonksiyonunun bildirimini de add.h içerisinde yapalım.
// add.c
int add(int val1, int val2)
{
return val1 + val2;
}
// add.h#ifndef ADD_H_
#define ADD_H_int add(int val1, int val2);#endif// main.c
#include "add.h"int val1 = 10;
int val2 = 20;int main(void)
{
int result = add(val1, val2);
return 0;
}
İlk önce main.c ve add.c kaynak dosyalarını aşağıdaki gibi derliyoruz.
gcc -c main.c -o main.o
gcc -c add.c -o add.o
Bu işlemden sonra artık elimizde iki tane ROF bulunmaktadır. Bu ROF’ların sembol tablosunu incelemek için aşağıdaki komutu kullanabiliriz.
readelf -s add.oSymbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS add.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 2
4: 0000000000000000 0 SECTION LOCAL DEFAULT 3
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 6
7: 0000000000000000 0 SECTION LOCAL DEFAULT 4
8: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 add
Yukarıda görüldüğü gibi add fonksiyonu için 0'dan başlayan bir adresleme yapılmıştır. add fonksiyonu asıl kullanılacağı yere linker tarafından link edilene kadar gerçek adrese sahip olmayacaktır.
Şimdi de main.o dosyasına bakalım.
readelf -s main.oSymbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 6
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 5
8: 0000000000000000 4 OBJECT GLOBAL DEFAULT 3 val1
9: 0000000000000004 4 OBJECT GLOBAL DEFAULT 3 val2
10: 0000000000000000 39 FUNC GLOBAL DEFAULT 1 main
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
12: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND add
Yine main.c içerisinde hem global değişkenlerin hem de add.c kaynak dosyasından gelen add fonksiyonun gerçek adres tanımlamaları henüz bulunmamaktadır.
Executable Nesne Dosyaları (Executable Object File)
EOF bir C veya C++ programının en son çıktısıdır. EOF’lar ROF’ların tüm içeriğine sahiptir çünkü EOF’lar tek bir ROF veya birden fazla ROF’un linking sürecinden sonra birleştirilmiş ve düzenlenmiş halidir.
EOF elde etmek için aşağıdaki komutla daha önceden oluşturduğumuz main.o ve add.o ROF’larını linker ile birleştirilerek nihai EOF’un elde edilmesini sağlıyoruz.
gcc add.o main.o -o test.out
Nihai olarak oluşan dosyanın sembol tablosunu inceleyelim.
readelf -s test.outSymbol table '.dynsym' contains 6 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
4: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
5: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@GLIBC_2.2.5 (2)Symbol table '.symtab' contains 65 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000238 0 SECTION LOCAL DEFAULT 1
2: 0000000000000254 0 SECTION LOCAL DEFAULT 2
3: 0000000000000274 0 SECTION LOCAL DEFAULT 3
4: 0000000000000298 0 SECTION LOCAL DEFAULT 4
5: 00000000000002b8 0 SECTION LOCAL DEFAULT 5
6: 0000000000000348 0 SECTION LOCAL DEFAULT 6
7: 00000000000003c6 0 SECTION LOCAL DEFAULT 7
8: 00000000000003d8 0 SECTION LOCAL DEFAULT 8
9: 00000000000003f8 0 SECTION LOCAL DEFAULT 9
10: 00000000000004b8 0 SECTION LOCAL DEFAULT 10
11: 00000000000004d0 0 SECTION LOCAL DEFAULT 11
12: 00000000000004e0 0 SECTION LOCAL DEFAULT 12
13: 00000000000004f0 0 SECTION LOCAL DEFAULT 13
14: 00000000000006b4 0 SECTION LOCAL DEFAULT 14
15: 00000000000006c0 0 SECTION LOCAL DEFAULT 15
16: 00000000000006c4 0 SECTION LOCAL DEFAULT 16
17: 0000000000000708 0 SECTION LOCAL DEFAULT 17
18: 0000000000200df0 0 SECTION LOCAL DEFAULT 18
19: 0000000000200df8 0 SECTION LOCAL DEFAULT 19
20: 0000000000200e00 0 SECTION LOCAL DEFAULT 20
21: 0000000000200fc0 0 SECTION LOCAL DEFAULT 21
22: 0000000000201000 0 SECTION LOCAL DEFAULT 22
23: 0000000000201018 0 SECTION LOCAL DEFAULT 23
24: 0000000000000000 0 SECTION LOCAL DEFAULT 24
25: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
26: 0000000000000520 0 FUNC LOCAL DEFAULT 13 deregister_tm_clones
27: 0000000000000560 0 FUNC LOCAL DEFAULT 13 register_tm_clones
28: 00000000000005b0 0 FUNC LOCAL DEFAULT 13 __do_global_dtors_aux
29: 0000000000201018 1 OBJECT LOCAL DEFAULT 23 completed.7698
30: 0000000000200df8 0 OBJECT LOCAL DEFAULT 19 __do_global_dtors_aux_fin
31: 00000000000005f0 0 FUNC LOCAL DEFAULT 13 frame_dummy
32: 0000000000200df0 0 OBJECT LOCAL DEFAULT 18 __frame_dummy_init_array_
33: 0000000000000000 0 FILE LOCAL DEFAULT ABS add.c
34: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
35: 0000000000000000 0 FILE LOCAL DEFAULT ABS crtstuff.c
36: 000000000000082c 0 OBJECT LOCAL DEFAULT 17 __FRAME_END__
37: 0000000000000000 0 FILE LOCAL DEFAULT ABS
38: 0000000000200df8 0 NOTYPE LOCAL DEFAULT 18 __init_array_end
39: 0000000000200e00 0 OBJECT LOCAL DEFAULT 20 _DYNAMIC
40: 0000000000200df0 0 NOTYPE LOCAL DEFAULT 18 __init_array_start
41: 00000000000006c4 0 NOTYPE LOCAL DEFAULT 16 __GNU_EH_FRAME_HDR
42: 0000000000200fc0 0 OBJECT LOCAL DEFAULT 21 _GLOBAL_OFFSET_TABLE_
43: 00000000000006b0 2 FUNC GLOBAL DEFAULT 13 __libc_csu_fini
44: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterTMCloneTab
45: 0000000000201000 0 NOTYPE WEAK DEFAULT 22 data_start
46: 00000000000005fa 20 FUNC GLOBAL DEFAULT 13 add
47: 0000000000201018 0 NOTYPE GLOBAL DEFAULT 22 _edata
48: 00000000000006b4 0 FUNC GLOBAL DEFAULT 14 _fini
49: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@@GLIBC_
50: 0000000000201014 4 OBJECT GLOBAL DEFAULT 22 val2
51: 0000000000201000 0 NOTYPE GLOBAL DEFAULT 22 __data_start
52: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
53: 0000000000201008 0 OBJECT GLOBAL HIDDEN 22 __dso_handle
54: 00000000000006c0 4 OBJECT GLOBAL DEFAULT 15 _IO_stdin_used
55: 0000000000000640 101 FUNC GLOBAL DEFAULT 13 __libc_csu_init
56: 0000000000201020 0 NOTYPE GLOBAL DEFAULT 23 _end
57: 00000000000004f0 43 FUNC GLOBAL DEFAULT 13 _start
58: 0000000000201018 0 NOTYPE GLOBAL DEFAULT 23 __bss_start
59: 000000000000060e 39 FUNC GLOBAL DEFAULT 13 main
60: 0000000000201018 0 OBJECT GLOBAL HIDDEN 22 __TMC_END__
61: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerTMCloneTable
62: 0000000000201010 4 OBJECT GLOBAL DEFAULT 22 val1
63: 0000000000000000 0 FUNC WEAK DEFAULT UND __cxa_finalize@@GLIBC_2.2
64: 00000000000004b8 0 FUNC GLOBAL DEFAULT 10 _init
EOF ELF içerisinde iki tane farklı sembol tablosu bulunmaktadır. Bunlardan ilki olan .dynsym dinamik sembol tablosu executable dosyanın load edilirken çözümlenecek olan sembolleri içermektedir. .symtab sembol tablosu içerisinde de çözümlenmiş (resolved) olan bütün semboller ve dinamik sembol tablosundan gelecek olan çözümlenmemiş (unresolved) semboller bulunmaktadır.
Görüldüğü gibi ROF içerisinde daha önceden gerçek adresleri tanımlanmamış olan global değişkenler ve add fonksiyonu artık EOF içerisinde gerçek adreslerine kavuşmuştur.
Statik Kütüphaneler (Static Libraries)
Statik kütüphane bir C veya C++ programından çıktı olarak alabileceğimiz ürünlerden bir tanesidir. Statik kütüphaneler ROF’lardan oluşan bir Unix arşivi diyebiliriz. Bir statik kütüphane linking aşamasında başka statik kütüphaneler veya ROF’larla birleştirilerek EOF elde etmek için kullanılır.
Statik kütüphaneyi tek başına bir nesne dosyası olarak düşünmemek gerekir. Statik kütüphane nesne dosyaları için sadece bir container oluşturuyor. Yani statik kütüphaneler bir ELF dosyası değildir basitçe UNIX ar aracı ile oluşturulan arşiv dosyalarıdır.
Linker linking aşamasında bir statik kütüphaneyi kullandığında ilk başta bu kütüphane içerisindeki ROF’ları çıkarır ve daha sonra tanımlanmamış sembolleri bu ROF’lar içerisinde arayarak link eder.
Statik kütüphaneler Unix sistemlerde .a uzantılı olarak Microsoft Windows’da ise .lib uzantısına sahiptir.
Şimdi daha önceden oluşturduğumuz toplama işlemini yapan fonksiyonumuzu içeren kaynak dosyasını ar aracını kullanarak bir statik kütüphane haline getirelim. Bu örnekte tek bir ROF dosyası olacak ancak bir statik kütüphane birden fazla da ROF dosyasından oluşabilir.
ar crs libadd.a add.o
Bu komutla birlikte artık libadd.a adında bir statik kütüphanemiz oluşmuştur.
Bu add.a statik kütüphanesini artık projemizde kullanabiliriz. Bunun için de linker’in hangi ROF ile link etmesi gerektiğini aşağıdaki komut aracılığı ile belirtmemiz mümkündür.
gcc main.o -L./ -ladd -o test.out
.L./ argümanı ile statik ve dinamik kütüphanelerin aranması gereken yollardan birini varsayılan yollara eklemiş oluyoruz. Buraya spesifik bir yol da belirtmek mümkün ancak ./ ile şu an çalıştığımız klasörü de arama lokasyonlarına ekle diyebilmekteyiz.
ladd ise libadd.a veya libadd.so adında bir kütüphaneyi işaret ettiğimizi belirtmektedir. Konvensiyon olarak -lx formatında yazıldığında libx olarak arama yapılır.
Bu komut çalıştırıldıktan sonra artık main.o ROF dosyası ile libadd.a statik kütüphanesi link edilerek nihai bir EOF ortaya çıkarılmıştır. Bu EOF dosyası artık statik bir kütüphaneye ihtiyaç duymadan çalışabilir çünkü linker aşamasından sonra herşey bir araya getirilerek tek bir dosya oluşturulmuştur. Ancak ileride libadd.a kütüphanesinin arayüzünde veya implementasyonunda bir değişiklik olması halinde hem statik kütüphane hem de nihai proje yeniden derlenmek zorundadır.
Bu yüzden statik kütüphane kullanımında binary dosya boyutu oldukça yüksek olmaktadır çünkü söylediğimiz gibi herşey statik olarak link edildiği için gerekli bütün komutlar tek bir nesne dosyasında toplanmaktadır.
Dinamik Kütüphaneler (Dynamic or Shared Libraries)
İsminden de anlaşılacağı üzere statik kütüphanelerden farklı olarak dinamik kütüphaneler nihai EOF’in bir parçası olarak yer almazlar. Program load edilirken dinamik kütüphaneler sürece dahil olur. Yani dinamik kütüphane kullanımında linking sürecinde çözümlenmeyen semboller bulunabilir. Bu semboller proses yüklendiği zaman aranır. Bu süreçleri dynamic linker (loader) yönetmektedir.
Dinamik kütüphaneler Unix sistemlerde .so, MacOS’de .dylib, Windows’da da .dll uzantısıyla tutulmaktadır.
Bir process yüklendiği zaman dinamik kütüphaneler yüklenerek process tarafından erişebilecek bir hafıza alanına map edilir. Bu prosedür loader tarafından yapılmaktadır.
Dinamik kütüphane oluşturmak için öncelikle daha önceden oluşturduğumuz test.c kaynak dosyasını derlememiz gerekmektedir. Bu aşamada statik kütüphane oluştururken kullandığımız komuta ek olarak bir seçenecek daha ekleyeceğiz.
gcc -c add.c -fPIC -o add.o
Burada ek olarak gelen -fPIC argümanı dinamik kütüphane için gerekli olan Position Independent Code (PIC) özelliğini sağlamaktadır. Böylelikle add.o adında PIC içeren bir ROF elde etmiş olduk.
PIC olarak oluşturulan ROF ile dinamik kütüphanemizi yaratmak için de aşağıdaki komutu kullanıyoruz. Bu işlemden önce daha önceden oluşturduğumuz statik kütüphaneyi libadd.a siliyoruz.
gcc -shared add.o -o libadd.so
Bu komutla birlikte artık libadd.so adında bir dinamik kütüphanemiz (.so shared object) oluşmuştur. Artık nihai EOF dosyamızı aşağıdaki gibi main.o ile link ederek oluşturabiliriz.
gcc main.o -L./ -ladd -o test.out
Bu işlemden sonra artık elimizde test.out adında bir EOF bulunmaktadır. Ancak bu EOF’yi aşağıdaki gibi çalıştırdığımızda hata ile karşılaşacağız.
./test.out ./test.out: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
Peki neden böyle bir hata aldık?
Statik kütüphane ile de aynı işlemleri yaptık ancak onda bu şekilde bir hata almamıştık çünkü statik linkleme sonrasında test.out içerisinde statik kütüphaneden gelen ilgili ROF çözümlenerek link edilip tek bir EOF haline getirildi. Yani bu aşamadan sonra herşey zaten test.out içerisindeydi bu yüzden test.out’u çalıştırdığımızda ek bir arama işlemi gerçekleştirmedi.
Ancak dinamik linking yaptığımızda test.out içerisinde PIC’e sahip henüz çözümlenmemiş bir ROF bulunmaktadır. Bu yüzden loader programın yüklenmesi sırasında bu kütüphaneyi arayıp bulması gerekir. Biz de daha henüz shared nesnelerin aranacağı yolu loader’a belirtmediğimiz için bir hata ile karşılaştık. Loader sadece default olarak kendisine tanımlı yollara baktı bulamayınca da doğal olarak hata oluşturdu.
Bu sorunu çözmek için loader’ın dinamik kütüphaneyi bulması için dosya yolumuzu çevresel değişken (Environment Variable) olarak eklemiz gerekmektedir. Bunun için aşağıdaki komutu kullanabiliyoruz.
export LD_LIBRARY_PATH=./
Bu komut ile birlikte loader’in varsayılan klasörler dışında şu anda çalıştığımız klasörde de arama yapmasını sağlıyoruz. Bu komuttan sonra artık programımız çalışacaktır.
Buradaki örnekte dinamik kütüphanenin loader tarafından otomatik olarak yüklenmesi örneği üzerinde durduk ancak dinamik kütüphaneler sadece otomatik olarak değil manual olarak da kaynak dosyası içerisinde proses’e dahil edilebilmektedir.
Geliştirmeler için kullandığımız IDE’lerde bu kütüphaneleri hazır olarak oluşturabileceğimiz araçlar bulunmaktadır. Örneğin aşağıda Eclipse IDE üzerinden bir C veya C++ projesi projesi yaratırken sunulan örnek bir ekran görüntüsünü paylaşıyorum.
Görüldüğü gibi hem EOF, hem dinamik hem de statik kütüphane oluşturmak için ön ayarları yapılmış hazır projeler bir çok IDE tarafından desteklenmektedir ve hazır olarak bulunmaktadır.
Bu yazımın amacı da aslında geliştirici olarak sürekli kullandığımız bu nesne dosyalarının ve kütüphanelerin arka planlarından bahsetmeye çalışmaktı.
Eclipse üzerinde statik ve dinamik kütüphanelerin nasıl oluşturulduğunu merak edenler için de hazırladığım iki video’nun linkini aşağıya bırakıyorum.
Diğer yazılarımda görüşmek üzere…