Category Archives: build

C – Linker ve Nesne Dosyaları


Header Dosyalarının yerleri ve Search Path Ayarları yazısının girişinde sorduğum sorulardan;

  • Bir Header dosyasının belirttiği bir object file nasıl bulunur? Linker bunu nerede arar?

sorularını burada cevaplayacağım. Bunu yaparken, Linker ve Linker parametrelerinden söz edeceğim.

Object File

Stackoverflow‘da Why compile to an object file first? sorusuna verilen cevaplarda önce object code‘a derleme konusunda aşağıdaki noktalara değiniliyor.

Bu olaya, separate compilation denir.

  • Aynı anda, farklı object file‘larda birden fazla kişi çalışabilir.
  • Daha hızlı derleme, aynı dosyaları tekrar tekrar derlemene gerek yoktur.
  • object file‘lar farklı dillerden derlenerek oluşturulmuş olabilir. Daha sonra, Linker ile birleştirilebilirler.
  • separate compilation birçok sistem geneli library‘nin dağıtımını kolaylaştırır. Bunlar static ya da shared library‘ler olabilir. Mesela; işletim sistemi kütüphaneleri, dillerin standart kütüphaneleri, ya da üçüncü parti kütüphaneler.

Yukarıda anlatılanlardan, Make vs. Ant – Build araçları ile konfigürasyon yönetimi yazısında bahsetmiştim. Burada, object file‘ları birleştirmek Linker‘ın işidir.

gcc ile Link işlemini, çeşitli parametreler kullanarak yapabiliriz. Ayrıca, binutils den biri olan ld ile de bu işlemi linker adımı içerisinde yapabiliriz.

GCC ve Linker Parametreleri

Options for Linking‘de link aşamasında kullanılabilecek olan parametreler açıklanıyor.

gcc -c file1.c file2.c

Yukarıdaki yapıdaki -c, compile demektir, bu parametre Derleyicinin kullandığı bir parametredir.  Belirtilen kaynak kodların object filelarını oluşturur. linker bu parametreyi, -S ve -E parametrelerinden biriyle işlem yapıldığında görmezden gelir.

gcc -o exe_dosya file1.o file2.o

Yukarıdaki yapıda, -o (output) parametresi belirtildiği için, belirtilen object file‘lar, Linker tarafından Link edilerek executable oluşturulur. Dolayısıyla, Linker‘ın en temel parametresi, .o uzantılı object file‘dır. Ama çoğu zaman tek başına .o lar ile işlem yapılmaz. Kütüphaneler(shared->.so ya da static->.a) ile işlem yapılır.

C ile LIBC’yi Kullanmadan Program Yazmak yazısında bahsettiğim -nostdlib‘e de değinmek isterim.

gcc -nostdlib -o hello hello.c

-nostdlib Linker‘a, .c dosyası derlendikten sonra oluşan .o dosyasını libc kütüphanesiyle link etmemesini söylüyor. Yoksa bu .o, .c dosyasında #include <stdio.h>, #include <stdlib.h> ... ifadeleri belirtilmemiş bile olsa standart kütüphaneyle link edilirdi. Bu parametre ile .o dosyasına link edilmeyecek olan libc kütüphanesi,

/usr/lib/gcc/i486-linux-gnu/4.4/libgcc.a

dir.

Bununla birlikte hazır bakmışken libgcc ile ilgili başka nerelerde neler var onlara da bakalım.

lifeinbeta@lifeinbeta:/$ sudo find / -name libgcc*
/opt/lampp/lib/libgcc_s.so.1
/usr/share/lintian/overrides/libgcc1
/usr/share/doc/libgcc1
/usr/lib/ure/lib/libgcc3_uno.so
/usr/lib/gcc/i486-linux-gnu/4.4/libgcc_s.so
/usr/lib/gcc/i486-linux-gnu/4.4/libgcc.a
/usr/lib/gcc/i486-linux-gnu/4.4/libgcc_eh.a
/usr/lib/libgccpp.so.1
/usr/lib/libgccpp.so.1.0.2
/var/lib/dpkg/info/libgcc1.md5sums
/var/lib/dpkg/info/libgcc1.shlibs
/var/lib/dpkg/info/libgcc1.postrm
/var/lib/dpkg/info/libgcc1.symbols
/var/lib/dpkg/info/libgcc1.postinst
/var/lib/dpkg/info/libgcc1.list
/lib/libgcc_s.so.1

-llib: Linker, belirtilen object file‘ı, burada belirtilen library ile link eder.

UNIX’te bir kütüphanenin ismi, “liblibrary_name(.ar | .so)” şeklindedir. Link edilmesi istenen kütüphane, -llibname şeklinde değil, -lname şeklinde belirtilmelidir.

gcc parametreleri belirtilirken, -l parametresinin hangi sırada belirtildiği önemlidir. Eğer, foo.o -lname zoo.o şeklinde bir ifade varsa, zoo.o, name kütühanesindeki gerekli .o ile link edilmez. Çünkü, Linker, object file‘ları ve kütüphaneleri, gcc komutunda bunların belirtildiği noktaya gelince arar ve sadece o noktada işlem yapar. Yukarıda, -lname, bütün object file‘lardan sonra belirtilmiş olsaydı, Linker, önce object file‘ları işleyecekti sonra en sonda belirtilen kütüphaneyi arayacaktı ve bulduğunda bunu, önceden farkettiği object file‘lar ile link edebilecekti.

Peki, yukarıda sadece library ismini belirttik. UNIX üzerinde yüzlerce dizin olduğunu hesaba katarsak, Linker belirtilen kütüphaneyi hangi dizinde bulacak? Linker, gerekli olan bir kütüphaneyi, bir standart dizin listesindeki dizinlerde arar. Daha sonra bu kütüphanenin içinden gerekli olan object file‘ın nasıl alınarak nasıl link edildiğini Loaders yazısında anlatacağım.

Library Search Path = Link Path = Load Path

setting search paths‘te ve When should i set LD_LIBRARY_PATH‘te belirtildiği gibi;

Linker, kütüphaneleri sırasıyla /usr/lib/ ve /usr/local/lib/ dizinlerinde arar. Bu dizinler, link edilecek dosyaların aranması için kullanılan, varsayılan dizinlerdir. Bu dizinlere, library search path ya da link path ya da load path denir. Standart C Kütüphanesi olan libgcc, /usr/lib/ altında bulunur ve varsayılan olarak her zaman link edilir. Link işleminde kullanılması gereken harici bir kütüphanenin bu dizin listesine eklenmesi gerekir.

Load Path‘e yeni bir dizin, LD_LIBRARY_PATH ortam değişkeni aracılığıyla eklenebilir(Ancak bu, güvenlik nedeniyle, tavsiye edilmez). Bu değişken, varsayılan dizinleri tutmaz, sadece harici olarak eklenmiş dizinleri tutar. Ortam değişkenine eklemekle, yeni eklenen dizin başka link işlemlerinde de aranması için kalıcı hale getirilmiş olur. Sadece uygulamaya göre, Linker tarafından içinde kütüphane aranacak olan, dizin belirtmek için(tavsiye edilen) -Ldizin parametresi kullanılmalıdır.

Ancak, -L parametresiyle link path‘e her link işlemi için dizin belirtmek istemeyiz. Aynı şekilde, -l parametresiyle link edilmesini istediğimiz library‘leri belirtirken de tekrar tekrar bu işi yapmak istemeyiz. Bunlara alternatif olarak, makefile kullanabiliriz. Aşağıda örnek bir Makefile var.

CC = gcc -Wall
SOURCE = hello.c
FLAGS = -lglut
BINARY = hellogl

all:
        $(CC) $(SOURCE) -o $(BINARY) $(FLAGS)

clean:
        @echo Temizleniyor...
        @rm $(BINARY)
        @echo Temiz!

freeglut3 Kurulumu

Şimdi, freeglut3‘ü sistemimize yükleyelim.

Bunun için, OpenGL Utility Toolkit(freeglut3) ü kurduktan sonra aşağıdaki işlemi yaptım.

lifeinbeta@lifeinbeta:/$ sudo find / -name freeglut*
/usr/share/doc/freeglut3
/var/cache/apt/archives/freeglut3_2.6.0-0ubuntu2_i386.deb
/var/lib/dpkg/info/freeglut3.postinst
/var/lib/dpkg/info/freeglut3.list
/var/lib/dpkg/info/freeglut3.postrm
/var/lib/dpkg/info/freeglut3.shlibs
/var/lib/dpkg/info/freeglut3.md5sums

Henüz .h ve .so dosyları yok. Daha sonra, development libraries and headers for GLUT(freeglut-dev)i kurdum ve aşağıdaki işlemi yaptım.

lifeinbeta@lifeinbeta:/$ sudo find / -name freeglut*
/usr/share/doc/freeglut3
/usr/share/doc/freeglut3-dev
/usr/share/doc/freeglut3-dev/freeglut.html
/usr/share/doc/freeglut3-dev/freeglut_user_interface.html
/usr/share/doc/freeglut3-dev/freeglut_logo.png
/usr/include/GL/freeglut_std.h
/usr/include/GL/freeglut.h
/usr/include/GL/freeglut_ext.h
/var/cache/apt/archives/freeglut3_2.6.0-0ubuntu2_i386.deb
/var/cache/apt/archives/freeglut3-dev_2.6.0-0ubuntu2_i386.deb
/var/lib/dpkg/info/freeglut3-dev.list
/var/lib/dpkg/info/freeglut3.postinst
/var/lib/dpkg/info/freeglut3.list
/var/lib/dpkg/info/freeglut3.postrm
/var/lib/dpkg/info/freeglut3.shlibs
/var/lib/dpkg/info/freeglut3-dev.md5sums
/var/lib/dpkg/info/freeglut3.md5sums

Header dosyaları, /usr/include/ içinde /GL/ dizini oluşturularak buraya konuldu. Neden buraya konduğu konusunda bkz. Header dosyalarının yerleri ve Search Path ayarları. Ancak hala kütüphaneler ortada yok. development libraries … dediğine göre yüklenmiş olmalılar, ama bulamadım. 🙂 How to install freeglut?‘dan bakarak bu kütüphanelerin libglut isminde olduğunu gördüm.

Aşağıdaki işlemi yaptığımda bu dosyaları görebildim.

lifeinbeta@lifeinbeta:/$ sudo find / -name libglut*
/usr/share/doc/libglut3-dev
/usr/lib/libglut.so.3
/usr/lib/libglut.so
/usr/lib/libglut.so.3.9.0
/usr/lib/libglut.a
/var/cache/apt/archives/libglut3-dev_3.7-25_all.deb
/var/lib/dpkg/info/libglut3-dev.list
/var/lib/dpkg/info/libglut3-dev.md5sums

Linkler

Leave a comment

Filed under build, C, gcc, linker

Make vs. Ant – Build Araçları ile Konfigürasyon Yönetimi


Konfigürasyon Ne Demek?

Bir uygulamadaki kodun satır sayısı arttıkça, uygulama üzerinde geliştirme yapan kişi sayısı arttıkça, bu kodları tek dosyada tutmak çok zor hale gelir.  Bu yüzden ana kaynak kod dosyası,  küçük kaynak kod dosyalarına bölünmüştür. Bu durumda, küçük kaynak kodların [uygulamanın çalıştırılabilir kodunu oluşturmak için birbirleri arasındaki kurdukları ilişkiler] -> [konfigürasyonu] belirtilmelidir.

Küçük dosyalar, ana dosyayı oluşturdukları için aralarında mantıksal bağlılıklar olacaktır. Bir dosyada yapılan değişiklik bu bağlılık nedeniyle diğer dosyaları etkileyecektir. Değişiklikler yapıldıktan sonra programı tekrar build ederken bu bağlılıklar(dependency) bir şekilde halledilmelidir. Bu yüzden, konfigürasyon yapılırken kullanılan dilin çalıştırılabilir kod oluşturma süreci göz önüne alınmalıdır.

Kullanılan Programlama Diline Göre Konfigürasyon

Programlama diline özgü konfigürasyonu oluşturabilmek için Build araçları yapılmıştır. C için, make; Ruby için, rake; Java için; Ant, Maven, Buildr, Raven ve diğer diller için bilmediğim başkaları vardır. Bu araçların farklılaşmasının sebebi; Make ve Ant için, kullanılan dilin derleyicilerinin, o dilde yazılmış olan kaynak kodu, çalıştırılabilir koda dönüştürme süreçlerindeki farklılıklardır. Rake‘de ve Rake‘in Java uyarlaması olan Buildr ve Raven‘de asıl sebep bu değildir.

Make ve Ant arasındaki fark == C ve Java ile Yazılmış Kaynak Kodların Derlenme Süreçleri arasındaki fark

Why is no one using make for Java? : Stackoverflow’daki bu soruda:

Neredeyse her Java projesinde build aracı olarak ya Maven ya da Ant kullanıldığını görüyorum. Bunlar güzel araçlar ve her projede kullanılabilirler ama, zaten make var, neden o kullanılmıyor? Java harici birçok projede kullanılıyor ve Java için de build işlemini yapabilecek kapasitede.

Make‘in Java’da kullanılmaması için make’in temel bir eksikliği mi var?

diye soruluyor. Verilen cevaplarda şunlar üzerinde duruluyor:

Java’da projelerin kaynak kod organizasyonu klasörler ve bunlar içerisindeki dosyaların hiyerarşisinden oluşur. C de ise düz(flatten) bir kaynak kod yapısı vardır. Make, dosya hiyerarşileri ile çalışmayı doğrudan desteklemez.

Make ile Ant arasındaki fark

Make, bir defa derlendikten sonra üzerinde değişiklik yapılan dosyaları belirleyebilme konusunda iyi değildir.

Ant ile, derlemeden sonra üzerinde sadece üzerinde değişiklik yapılan dosyalar bir defada belirlenip tek bir derleme adımında sadece bu dosyalar tekrar derlenir. Ant yerine make olsaydı, belirli bir bağlılık kuralında belirtilen dosyalardan, üzerinde değişiklik yapılsın yapılmasın, her dosya için javac‘ın ayrı ayrı çağrılması gerekecekti.

Make‘in temel prensibi; bir bağlılık(dependency) belirtilir ve bu bağlılığı çözebilecek(resolve) bir kural yazılır. Bu süreç, C de genel olarak şöyledir:

main.c yi  main.o ya dönüştürebilmek için cc main.c çağrılır. Bu işlemi Java’da,

Main.java yı, Main.class a dönüştürmek için javac Main.java diyerek yapabiliriz. Ancak,

javac Main.java 
javac This.java 
javac That.java 
javac Other.java 

ile

javac Main.java This.java That.java Other.java 

arasında gece-gündüz kadar belirgin bir fark vardır. Ayrıca, yüzlerce sınıf olduğunda bu fark dayanılmaz bir çileye dönüşecektir.

Bu sebeplerden dolayı Ant ve Maven gibi make‘e alternatif build araçlarına ihtiyaç duyulmuştur.

Diğer bir cevapta, Greg Hewgill,

Yıllanmış make aracı, C ve C++ gibi ayrı ayrı derlenen(seperately compiled) diller için iyidir.

C’de, bir modülü derlerken, derlenecek dosya için tek bir object file oluşturulur.

Yani, derleme işlemi bir defada bir .c dosyasını derleyip bir .o dosyası oluşturmak şeklindedir.

Linker ile, ayrı bir, derlenmiş .o dosyalarını çalıştırılabilir binary koda entegre etme aşaması vardır.

NOT: Bu aşamalar:

Derleme ve bunun sonucunda .o dosyası oluşturma,

gcc -c file1.c    // file1.o oluşur.

gcc -c file2.c   // file2.o oluşur.

gcc -o exe_dosya file1.o file2.o   //exe_dosya oluşur.

Yukarıdaki aşamalar tek bir komutta birleştirilip aşağıdaki gibi yazılabilir,

gcc -o exe_dosya file1.c file2.c

şeklindedir.

Java‘da ise import ile bir sınıfın kaynak koduna dahil edilen bütün sınıfların hepsi bu yeni sınıfla birlikte tekrar derlenir.

(C deki gibi önceden derlenmiş bir kod yoktur.)

Buna rağmen, derlenenecek olan Java kodundaki bağlılıklar belirtilip bu bağlılıkları çözen kurallar makefile içinde yazılabilir. Bu durumda, make, sınıfların doğru sırada derlenmesini sağlayarak build işlemini gerçekleştirebilir. Ancak, makeCircular Dependency‘leri(karşılıklı bağlılıklar) çözemez.

NOT: Çalıştırılabilir dosyanın(executablebyte sayısı, önceden derlenmiş dosyalardaki(#include ile belirtilen .o dosyaları) toplam byte sayısı + yeni derlenecek olan dosyanın üreteceği toplam byte sayısı demektir. Yani .o dosyalarının byte sayıları toplamıdır. Bunu görmek için, Hello from a libc-free world! yazısında -nostdlib bayrağı ile derleme işleminin sonucuna bakınız.

jesstess@kid-charlemagne:~/c$ cat hello.c
#include <stdio.h> int main() { printf("Hello World\n"); return 0; }
jesstess@kid-charlemagne:~/c$ gcc -o hello hello.c
jesstess@kid-charlemagne:~/c$ wc -c hello
10931 hello
jesstess@kid-charlemagne:~/c$ cat hello.c
int main() { char *str = "Hello World"; return 0; }
jesstess@kid-charlemagne:~/c$ gcc -o hello hello.c
jesstess@kid-charlemagne:~/c$ wc -c hello
10892 hello
jesstess@kid-charlemagne:~/c$ gcc -nostdlib -o hello hello.c,
jesstess@kid-charlemagne:~/c$ wc -c hello
1329 hello

NOT: Burdan sonraki kısım, bu yazıya bağlı Make yazısını yazarken aklıma geldi ve buraya 15.05.2011 Tarihinde eklendi.

Bu yazıda bahsettiğim Separate Compilation(ayrı ayrı derleme) mevzusunu açalım:

Ana dosyayı küçük dosyalara bölerken, amacımızın Separete Compilation olduğunu söylemiştim. Bunun aşağıdaki avantajları vardır:

  1. CVS gibi versiyon kontrol sistemleri, bir dosyana aynı anda birden fazla kişi tarafından yazılmasına izin vermez. Bu nedenle ana dosyayı küçük parçalara bölersek herkes bir küçük parça üzerine yazabilir hale gelir.
  2. Tekrar Derleme(recompilation) hızı artar. Kodda sık sık yeni eklemeler yapıldığında tüm programı derlemek zorunda kalmak yerine, program küçük parçalara bölündüğü için, sadece değişiklik yapılan küçük parçaları derlemek yeterli olacaktır.
  3. Kodu yönetmek(değişiklik ve debug) daha kolay hale gelir. Sadece değişikliği yapacağımız küçük dosyayı bulur orada değişiklik yaparız.
  4. Program modüler hale gelir. Belli veriler ve bunlar üzerinde işlem yapan fonksiyonlar bir arada tutulur. Bu şekilde diğer kısımlardan ayrılır. Sonraki projelerde bu kısımları kullanmak kolay hale gelir.(NOT: .lib, .ar, .so, .dll gibi kütüphaneler ile modüller oluşturulur.)
Şimdi ayrı ayrı derlemenin nasıl yapıldığını görelim.
Öğrencilerin aldıkları bir dersin final notlarını hesaplayan bir program yapalım. Bu program şu parçalardan oluşacak:
  1. student class, bu sınıfın örnekleri her öğrenci için o öğrencinin bilgilerini tutacak.(isim, öğrenci notu, vize notları vs..)
  2. Notları belli bir eğriye(normal dağılım vs.) uyduracak olan formülleri uygulayan fonksiyonlar
  3. main program
Kodun iskeleti aşağıdaki gibi olacak:
class student
{
    ...
}

string student::name() { ... }
int student::studentNo() { ... }
...

float linearScale(...) { ... }
float quadraticScale(...) { ... }
float bellCurve(...) { ... }

int main()
{
    ...
}
Bu programı parçalara bölerken her parçayı iki ayrı parça olarak ifade edeceğiz. Oluşan parçalarda ayrı ayrı derleme yapacağız. Bunun için,
  1. Öncelikle, programı parçalara böleriz. Her bir parça ya bir sınıftan ya da birbiriyle ilişkili fonksiyonların oluşturduğu bir gruptan oluşacak. Dolayısıyla, yukarıdaki program için; bir tane student sınıfını tutan parça olacak, bir tane eğri uydurma fonksiyonları için olacak, bir tane de main fonksiyonu içerecek olan parça olacak.
  2. Sonra, oluşan her parça bir header bir de source olmak üzere iki parçayla ifade edilir. Header, source‘da gerçeklemeleri yazılacak olan fonksiyonları belirten bildirimleri(declaration) ya da sınıfın verilerini – durumunu(data – state) tutacak.
Dolayısıyla, elimizde aşağıdaki beş dosya olmuş oldu:
    1. student.h, student sınıfı state‘ini tutar.
    2. student.c, student sınıfı için, student’in state’i üzerinde uygulanacak behavior‘u tutar.
    3. scaling.h, ölçekleme fonksiyonlarının declaration‘larını(prototiplerini) tutar.
    4. scaling.c, ölçekleme fonksiyonlarının gerçeklemelerini tutar.
    5. main.c, ana programı tutar.
NOT: Header Dosyaları
Derleyici, gerçeklemelerin bulunduğu kaynak kodu derlemeden önce header dosyasını okur. Burada, derleyeceği dosyayı derlerken bilmesi gereken bilgiler-bildirimler yer alır. Ayrıca, Make, bağlılıkları header ile kontrol eder.
Programızın kodu en son şöyle oldu:
// student.h
class student
{
            ...
}

// student.C
#include "student.h"

string student::name() { ... }
int    student::studentNo() { ... }
...

// scaling.h
float linearScale(...);
float quadraticScale(...);
float bellCurve(...);

// scaling.C
#include "scaling.h"

float linearScale(...){ ... }
float quadraticScale(...) { ... }
float bellCurve(...) { ... }

// main.C
#include "student.h"
#include "scaling.h"

int main()
{
    ...
}
Çalıştırılabilir dosyayı oluşturabilmek için .c dosyalarını ayrı ayrı derleyip oluşan .o dosyalarını link etmek gerekir.
% g++ -c student.C
% g++ -c scaling.C
% g++ -c main.C

% g++ -o grade student.o scaling.o main.o

C ve Java derlenme süreçleri konusunda daha detaylı bilgi için Intro to Reverse Engineering Software – Chapter 2 – The Compilation Process‘e bakılabilir.

Güzel bir söz
Everything we hear is an opinion, not a fact. Everything we see is a perspective, not the truth.
[Duyduğumuz herşey bir fikirdir, bir gerçek değil. Gördüğümüz herşey bir bakış açısıdır, bir doğru, değildir.]
– Marcus Aurelius

4 Comments

Filed under build