Kaynak dosyasından binary dosyasına dönüşüm. Bir C programının derlenme aşamaları - 1 (Preprocessing)
Gömülü yazılım geliştirme süreçlerinde geliştirici genelde bir IDE (Integrated Development Enviroment) üzerinde çalışır, yazılım isterlerini karşılayan kaynak (source) ve başlık (header) dosyalarını oluşturur yazdığı kodu derledikten sonra ilgili binary dosyayı donanıma IDE aracılığı ile yükleyip yazdığı kodun bir donanımda hayat bulmasını sağlar. Bu süreç iteratif olarak donanım ürün haline dönüştürülene kadar devam eder. Derleme sırasında çoğu geliştirici IDE tarafından arka planda tamamlanan bu süreçleri çok da merak etmez aslında. İşlemciye gömülen nihayetinde bir binary dosyadır ve debug işlemlerinde bu binary dosyadan ziyade ekrandaki C veya C++ kodları ile işimiz olur sadece. Bu yazı serisini gizemli bir şekilde arka planda gerçekleşen C kodlarının binary dosyaya dönüşene kadar ki serüvenini anlatmak için kaleme alacağım.
Preprocessor tanımlamaları C dilinde yaygın olarak kullanılmaktadır ve genelde aşağıdaki amaçlara yönelik kullanırız.
- Sabit sayılar ve string literal tanımlamaları yapılırken
- C fonksiyonu yerine fonksiyonel makro (Function-like Macros) tanımlanırken
- Başlık dosyası korumalarında (Header Guard)
- Koşulsal Derlemede (Conditional Compilation)
- Kod üretiminde (Code Generation)
Yukarıda ifade edilen kullanım amaçlarına ek olarak makro tanımlamaları DSL (Domain Specific Language) tasarlanırken de yoğun olarak kullanılmaktadır. Örneğin Google Test ve Cpputest gibi unit test frameworkleri içerisinde test senaryoları ve assertion’ları yazılırken preprocessor ifadeleri çok yoğun bir şekilde kullanılmıştır. Bu kullanım ayrıca framework’e bilginin kapsüllenmesi (encapsulation) özelliğini de kazandırmada rol almaktadır.
Bir editörde veya IDE üzerinde yazdığımız C kodunu derlediğimizde arka planda aşağıda belirtilen süreçler sırasıyla işlemektedir.
- Preprocessing
- Compilation
- Assembly
- Linking
Preprocessing sürecinde C dilinde yer alan tüm kaynak ve başlık dosyalarındaki preprocessor tanımlamaları çözümlenerek tek bir body olacak şekilde bir araya toplanır. Bu sürecin sonunda üretilen koda translation veya compilation unit ismi verilmektedir. Uzantısı da .i şeklindedir. Translation unit içerisinde hiçbir preprocessor ifadesi yer almamaktadır. Burada preprocessor ifadeleri # ile başlayan #include, #define, #pragma gibi ifadelerdir.
Bu yazı serisinde örnekleri gcc compiler ile herhangi bir IDE kullanmadan anlatmaya çalışacağım. Örnek amacı ile de aşağıda paylaştığım debug.c ve debug.h dosyaları oluşturulmuştur.
Yukarıda örnekte yapılan çok basit bir şekilde aslında custom bir debug print makro fonksiyonu oluşturmaktır. Sistemde tek bir kaynak ve başlık dosyası bulunmaktadır. Preprocessing operasyonunu gcc’nin -E komut satırı argümanını kullanarak gerçekleştirebilmemiz mümkündür.
gcc -E debug.cVeya dosya halinde translation unit dosyasının üretilmesi istenirsegcc -E debug.c > debug.i
Bu komutlar ile preprocessing süreci işletilir ve preprocessor ve makro ifadeleri çözümlenip başlık dosyaları da sisteme dahil edilerek tek bir kod parçası haline getirilir. Bu işlem sonrasındaki çıktının son kısmı aşağıda belirtilmiştir.
# 868 "/usr/include/stdio.h" 3 4# 5 "debug.h" 2
# 2 "debug.c" 2# 3 "debug.c"
int main(void)
{
printf("DEBUG: "); printf("Logging is started\n");
printf("DEBUG: "); printf("Program is started\n");return 0;
}
Burada da görüldüğü gibi preprocessing sürecinden sonra üretilen kodda hiçbir preprocessor ifadesi yer almamaktadır. Hem başlık dosyasının kendisi hem de içindeki makro ifadeler çözümlenerek preprocessor ifadelerden arındırılmıştır. Bu ifadeleri çözümleyen bir parser bulunmaktadır ve bu parser’in C dili hakkında herhangi bir bilgisi yoktur yani preprocessing sürecinde siz başlık dosyasında dilde olmayan anlamsız bir ifade de yazsanız yine bu preprocessing sürecinden geçecektir.
Örneğin yukarıdaki kod preprocessing sürecinden geçtikten sonra aşağıdaki kod parçası üretilmektedir.
# 1 "test.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test.c"Buraya sacma bir XXX yaziyorum.int main(void)
{
return 0;
}
Görüldüğü gibi preprocessor belli başlı temel işlevleri yapmaktadır ve C dili ile ilgili herhangi birşey bilmemektedir yani buradaki parser C gramerinden bağımsız bir parser olarak görev yapmaktadır. Preprocessor görevini tamamladıktan sonra artık Compiling süreci ile işlem devam etmektedir.
Ek bir bilgi olarak eğer compiler’a .i uzantılı olan translation unit dosyasını hazır sunarsanız preprocessing aşaması olmadan direk ikinci aşama olan Compilation sürecine geçilir.
Diğer yazılarımda görüşmek üzere…