32 Bit İşletim Sistemlerinde Çekirdek Kodlarının Derlenmesi Kaan Aslan 6 Mart 2003 1. Giriş İşletim sistemi geliştiricilerinin çözmek zorunda olduğu problemlerden biri de yazılan çekirdek kodun hiçbir yükleme bilgisi içermeyecek biçimde elde edilmesi ve yüklemeye hazır biçime getirilmesidir. Ticari amaçla oluşturulmuş derleyici ve bağlayıcı sistemleri genellikle işletim sistemi yazarları için kolaylıklar sunmazlar. Çünkü ticari sistemlerde amaç yeni bir sistem oluşturmak değil, mevcut sistem üzerinde uygulamalar ve araçlar geliştirmektir. Yeni bir sistemin oluşturulması çoğu kez işletim sistemini satan şirketlerin istemediği bir durumdur. Bu nedenle işletim sistemi geliştirebilmek için gereken uygun araçlar daha çok araştırma kurumları tarafından ya da bu konuda çalışan kişilerin bireysel gayretleri ile oluşturulmaktadır. Çekirdek kodların yüklemeye hazır hale getirilmesi için yaygın olarak kullanılan ticari derleyiciler ve bağlayıcılar kullanışsız kalmaktadır. Bu derleyicilerin kullanıldığı uygulamalarda amaç kodun ya da çalışabilen kodun istenilen biçime dönüştürülmesi için ek birtakım araçlara gereksinim duyulur. Düz ikili dosyaların oluşturulmasında GNU lisansı ile kullanıma sunulan nasm ve gcc derleyicileri ile ld bağlayıcılarının iyi bir araç olduğu söylenebilir. Pek çok işletim sistemi projesinde derleme araçları olarak bu programlar kullanılmaktadır. 2. Düz İkili Dosya Nedir? Bir işletim sisteminde derleme işlemi yapıldığında üretilen amaç kodun belli bir formatı vardır. Örneğin, Win32 sistemleri COFF formatını, GNU/Linux sistemleri temel olarak ELF formatını kullanırlar. Amaç kodlar tipik olarak bağlayıcının modülleri birleştirebilmesi için gereken bilgileri bulundurmaktadır. Bağlayıcı bu modülleri girdi olarak alır ve işletim sistemine uygun bir çalışabilir kod oluşturur. Bir sistemde derlenmiş ve çalışabilir duruma getirilmiş olan programlar tipik olarak dört kısma sahiptir. 2.1. Derleyicilerin Başlangıç Kodu (Start-up Code): Proses başlatıldığında çalışma derleyici tarafından yerleştirilmiş bir başlangıç kodundan başlar. Bu prosesin gerçek giriş noktasıdır. Yükleyiciler prosesi yarattıklarında kontrolün aktarılacağı adresi çalışabilen dosyanın başlık kısmından alırlar. Şüphesiz bu adres bağlayıcı tarafından amaç koddaki bilgilerden hareketle oluşturulmuştur. Yükleyici çalışabilen dosyayı yükledikten sonra kontrolü formatta belirtilen adrese geçirir. Bu başlangıç kodu çeşitli önişlemleri yaparak programlama dilince öngörülen giriş fonksiyonunu çağıracaktır. Örneğin C’de main fonksiyonu derleyicilerin başlangıç kodları tarafından çağrılır. Başlangıç kodları aynı zamanda varsayılan çıkış işlemlerini de yapar. Örneğin tipik olarak C’de akış main fonksiyonunu bitirdiğinde ya da main fonksiyonu içerisinde return işlemi yapıldığında akış yeniden başlangış koduna dönmekte ve proses orada sonlandırılmaktadır. Başlangıç kodu temel olarak aşağıdaki gibi bir yapıya sahiptir: 1 Kaan Aslan Makale Arşivi – www.kaanaslan.net Derleyicilerin başlangıç kodları genellikle normal bir modül olarak bağlama işlemine sokulur. Process’in gerçek başlangıç noktası başlangıç kodundaki modülde belirlenir. Bu modül içerisinde programlama dilinin belirlediği giriş fonksiyonu extern olarak bildirildiğinden bu fonksiyon bağlayıcı tarafından programı oluşturan diğer modüllerde otomatik olarak aranacaktır. C’de main fonksiyonunun hiçbir modülde bulunmamasından dolayı oluşan bağlama hatasının nedeni budur. Derleyicilerin başlangıç kodları işletim sisteminin çekirdeği için gereksiz ve zararlıdır. Başlangıç kodlarında derleme işleminin yapıldığı işletim sistemine özgü pek çok işlemler yapılmaktadır. Buradaki işlemler geliştirilmekte olan işletim sistemi için bir anlam taşımaz. Neyse ki derleyicinin başlangıç kodundan kurtulmak çok kolaydır. Tek yapılacak şey bağlama aşamasında giriş kodunun bulunduğu modülü bağlama işlemine dahil etmemektedir. Düz ikili dosyalarda derleyicilerinin giriş kodları bulunmamalıdır. 2.2. Çalışabilen Dosyanın Başlık Kısmı: Çalışabilen dosyalar yükleyici için bilgiler bulunduran başlık kısımlarına sahip olurlar. Başlık kısımlarındaki bilgiler derleme işleminin yapıldığı sisteme ilişkin oldukları için işletim sistemi geliştiricileri için gereksiz ve zararlıdırlar. İşletim sisteminin çekirdek kodlarının herhangi bir başlık kısmı olmamalıdır. Ancak maalesef hemen hemen tüm ticari derleyiciler başlık kısmını oluşturmak isterler. Başlık kısmı manuel olarak sonradan dosyadan atılabilse de bu durum ek bir yük oluşturmaktadır. Düz ikili dosyalarda böyle bir başlık kısmı olmamalıdır. 2.3. Sisteme Özgü Bilgiler: Çalışabilen dosyada derleme işleminin yapıldığı sisteme özgü olan birtakım özel bölümler oluşturulmuş olabilir. Örneğin Win32 sistemlerinde import tablosu, export tablosu, kaynak bölümü tamamen Win32 sistemlerine özgü bölümlerdir. Bu bölümlerin işletim sisteminin çekirdek kodlarında bulunmaması gerekir. Ancak pek çok derleyici sisteminde bu bölümler default olarak oluşturulmaktadır. Bu bölmlerin dikkate alınmaması ve dosydan çıkartılması manuel yöntemlerle mümkün olabilmektedir. 2.4. Gerçek Kod ve Veriler: Nihayet derlenmiş ve bağlanmış olan çalışabilen kod işlemcinin yürütebileceği kod ve verilere sahiptir. Tipik bir çalışabilen kodda programı oluşturan kod, statik veri ve yığın alanları vardır. Pek çok sistemde bu alanlara bölüm (section) denilmektedir. Bölümleri oluşturan sayfalar bellek üzerinde aynı koruma özellikleriyle biçimlendirilir. Tipik olarak kodun bulunduğu bölüm .text, ilkdeğer 2 Kaan Aslan Makale Arşivi – www.kaanaslan.net verilmiş statik verilerin bulunduğu bölüm .data, ilkdeğer verilmemiş statik verilerin bulunduğu bölüm .bss ve yığın bölümü de .stack biçiminde isimlendirilir. 2.5. Özetle... Düz ikili dosyalar giriş koduna ve başlık kısmına sahip olmayan, sisteme özgü bölüm içermeyen dosyalardır. Düz ikili dosyalar yalnızca kod ve statik veri içerirler. Düz ikili dosyalar bellekte bir adrese blok olarak yüklendiğinde problemsiz çalışabilmelidirler. Bu dosyalarda derleme işleminin yapıldığı sisteme yönelik hiçbir ek bilgi bulunmamalıdır. 3. Düz İkili Dosyalar Nasıl Elde Edilir? Yaygın kullanılan ticari derleyici sistemlerinin düz ikili dosya elde edilmesine yönelik özellikleri yoktur. Bu özlliklere sahip araçlardan en yaygın olanları GNU lisansıyla yazılmış olan nasm (netwide assembler) sembolik makina dili derleyicisi, C/C++ derleyicisi ve ld bağlayıcıdır. Bu geliştirme araçları tipik olarak GNU/Linux sistemlerinde kullanılırlar. Ancak bunların Win32 sistemlerinde çalışan uyarlamaları da vardır. Fakat maalesef Win32 sistemlerinde çalışan uyarlamalarında düz ikili dosya oluşturma konusunda problemler gözlenmiştir. Bu nedenle düz ikili dosyaların bu araçlarla GNU/Linux sistemlerinde derlenmesi iyi bir seçenektir. Tabi bu durum işletim sisteminin tamamen GNU/Linux sistemlerinde geliştirilmesini zorunlu hale getirmez. 32 bit işletim sistemlerinin geliştirilmesi başka başka sistemlerde ve başka derleyicilerle yapılabilir. Ancak düz ikili dosya elde etmek için geliştirme işlemi bittiğinde bu sistemlerde derleme ve bağlama yapılmalıdır. (Bu durumda kodun taşınabilir yazılması ve derleyicilere özgü birtakım eklentilerin kullanılmaması gerekir. Eğer eklentiler kullanılacaksa bunun son derlemenin yapılacağı sisteme uygun olması anlamlıdır.) 4. Sembolik Makina Dilinde Yazılan Kodların Düz İkili Formata Dönüştürülmesi Düz ikili dosya üretebilen en yaygın sembolik makina dili derleyicisi nasm (Netwide Assembler)’dır. nasm pek çok amaç kodu destekleyen bir sembolik makina dili derleyicisidir. Bu derleyici Linux a.out, ELF, NetBSD/FreeBSD, COFF ve Microsoft 16-bit OBJ amaç kod formatlarını destekler. GNU lisansıyla yazılmış olan bu derleyicilerin Linux, DOS ve Win32 uyarlamaları Internet’te pek çok siteden ücret ödemeksizin indirilebilir (http://nasm.sourceforge.net/). Derleme için gereken komut satırı argümanlarının yalın biçimi şöyledir: nasm -f [-o <dosya ismi> ] -f seçeneği üretilecek amaç kodun formatını belirtir. –o seçeneği ile üretilecek amaç kodun ismi verilebilir. Eğer –o seçeneği kullanılmazsa dosya isminin sonuna, –f ile belirtilen dosyanın formatı DOS ya da Win32 sistemlerinde kullanılan formatlardan biriyse .obj, Unix/Linux sistemlerinde kullanılan bir formatlardan biriyse .o eklenir. İkili dosyanın üretilmesi durumunda –o belirtmesi yapılmazsa üretilecek dosyaya herhangi bir uzantı eklenmez. Örneğin: 3 Kaan Aslan Makale Arşivi – www.kaanaslan.net nasm -f elf x.asm nasm –f coff y.asm ilk derlemeden ELF formatında x.o, ikinci derlemeden COFF formatında y.o dosyaları elde edilir. Seçeneklerden sonra boşluk bırakılmayabilir. Örenğin: nasm -felf x.asm Liste dosyası (assembly listing file) almak için –l seçeneği komuta eklenmelidir. Örneğin: nasm kernel.asm -l kernel.lst Düz ikili dosya elde edebilmek için –f bin seçeneği kullanılır. Örneğin: nasm –f bin kernel.asm –l kernel.lst –o kernel.bin ile kernel.asm dosyası derlenecek, kernel.lst isimli liste dosyası ile kernel.bin isimli düz ikili dosya elde edilecektir. -f seçeneği hiç girilmezse default olarak zaten düz ikili dosya üretilmektedir. Yukarıdaki derleme işlemi ile aşağıdaki tamamen eşdeğerdir: nasm kernel.asm –l kernel.lst –o kernel.bin işlemi ile kernel isimli düz ikili dosya elde edilir. nasm derleyicisinin yukarıda açıkladıklarımızın dışında pek çok seçeneği vardır. Örneğin –u, -e, -a gibi… Bu seçeneklerle ilgili ayrıntılı bilgileri nasm dokümanlarından elde edebilirsiniz (http://nasm.sourceforge.net/) 5. C’de Yazılan Kodların Düz İkili Formata Dönüştürülmesi C’de yazılan kodlar ilk aşamada gcc derleyicisi ile –c seçeneği kullanarak derlenmelidir. Bilindiği –c seçeneği “yalnızca derle” anlamına gelmektedir. Kaynak dosya –c seçeneği ile derlendiğinden .o uzantılı amaç dosya oluşturulmuş olur. Bu amaç dosyadan düz ikili dosyanın elde edilmesi için ld programından faydalanılabilir. Aşağıdaki C programı üzerinden ilerleyelim: test.c int g_x = 10; void func(void) { } 4 Kaan Aslan Makale Arşivi – www.kaanaslan.net int main(void) { func(); return 0; } ld bağlayıcısı düz ikili dosya elde etmek için aşağıdaki örnekte belirtildiği gibi kullanılabilir: ld test.o –o test.bin –Ttext 0x0 –e main --oformat binary -N Burada test.o daha önce –c seçeneğiyle derlenmiş olan amaç dosyayı belirtmektedir. Örneğimizde –o test.bin üretilecek düz ikili dosyayının ismini belirlemek için kullanılmıştır. Yani işlemden sonra test.bin dosyası üretilecektir. Eğer bu belirleme yapılmazsa düz ikili dosya a.out ismiyle üretilir. –T seçeneği ile bölümlerin yerleştirileceği adresler belirlenebilir. Bölüm ismi text, data, bss gibi herhangi bir bölüm ismi olabilir. Örneğimizde text bölümü sıfır adresinden başlatılmıştır. Bölümün yeri default olarak hex sistemde belirtilmektedir. Yani yer belirten sayıyı 0x seçeneği ile başlatmasaydık da yine sayı hex olarak yorumlanacaktı. Bölümlerin yerleri offset üretiminde etkili olur. Bağlayıcı en düşük bölümü dosyanın başına alarak dizilimi yapar. Örneğin test.c programı aşağıdaki gibi bağlanmış olsun: ld test.o –o test.bin –Tdata 0x50 –Ttext 0x100 –e main --oformat binary -N Burada düz ikili dosya data bölümü ile başlatılacak ve data bölümünün ilk sembolü olan 0x50 offsetinde bulunacaktır. Daha sonra dosyaya 0x50 ile 0x10 arasında boşluk verilecek ve func fonksiyonunun kodu 0x100’den başlatılacaktır. Bölümlerin düzenlenmesi sonraki bölümde ele alınmaktadır. Çalışabilen kodun başlangıç noktası –e ile belirlenebilir. Gerçi düz ikili dosyalarda bu belirlemenin bir önemi yoktur. Ancak bağlayıcının uyarısını kesmek için bu seçenek eklenebilir. Örneğimizde –e main biçiminde bir belirleme yapılmıştır. (Burada main yerine herhangi bir fonksiyon olabilirdi.) --oformat seçeneği bağlayıcının üreteceği dosyanın formatını belirlemekte kullanılır. Düz ikili dosya için, --oformat binary seçeneği kullanılmalıdır. Normal olarak ld bağlayıcısı bölümleri sayfa başlarına hizalar. Yani örneğin, text bölümünü izleyen data bölümü text bölümünün hemen aşağısında değil bir sayfa (Intel mimarisinde 4K) sonra sayfa başında bulunacaktır. –N seçeneği bunu engellemek için kullanılmakatadır. Şimdi test.c dosyasından düz ikili dosyanın elde edilmesini adımlarla yeniden açıklayalım. 5 Kaan Aslan Makale Arşivi – www.kaanaslan.net 1. Adım: gcc –c test.c 2. Adım: ld test.o –o test.bin –Ttext 0x0 –e main --oformat binary –N işlem sonrasında test.bin dosyası oluşacaktır. Bu dosyayı bir disassembler programıyla inceleyebilirsiniz. Disassembler olarak nasm ile aynı pakette bulunan ndisasm programını kullanabilirsiniz: ndisasm –b32 test.bin -b32 seçeneği 32 bit modda görüntüleme yapmak için kullanılır. Yukarıdaki programı disassembler altında şöyle görmelisiniz: 00000000 00000001 00000003 00000005 00000006 00000007 00000009 00000010 00000011 00000013 00000016 0000001B 0000001D 0000001F 00000020 00000022 00000023 00000024 0000002A 00000030 00000032 55 89E5 89EC 5D C3 89F6 8DBC2700000000 55 89E5 83EC08 E8E5FFFFFF 31C0 EB01 90 89EC 5D C3 8DB600000000 8DBF00000000 0A00 0000 push ebp mov ebp,esp mov esp,ebp pop ebp ret mov esi,esi lea edi,[edi+0x0] push ebp mov ebp,esp sub esp,byte +0x8 call 0x0 xor eax,eax jmp short 0x20 nop mov esp,ebp pop ebp ret lea esi,[esi+0x0] lea edi,[edi+0x0] or al,[eax] add [eax],al Burada gördüğünüz gibi data bölümünde bulunan g_x global değişkeni –N seçeneği sayesinde hemen text bölümünden sonraya yerleştirilmiştir. Aynı dosyayı –N seçeneğini kullanmadan düz ikili formata dönüştürmeyi deneyip karşılaştırma yapabilirsiniz. 5.1. Bölümlerin Organizasyonu Çalışabilen dosya tipik olarak text, data, bss ve stack bölümlerinden oluşur. Fonksiyonların makina kodları text bölümüne, ilkdeğer verilmiş statik ömürlü nesneler data bölümüne ve ilkdeğer verilmemiş statik ömürlü nesneler ise bss bölümüne yerleştirilir. stack programın stack alanını belirtmektedir. Örneğin: 6 Kaan Aslan Makale Arşivi – www.kaanaslan.net int g_a = 10; int g_b[100]; int main(void) { int i; g_a = 10; for (i = 0; i < 100; ++i) g_b[i] = 0; return 0; } gibi bir programda g_a data bölümüne g_b ise bss bölümüne yerleştirilir. main fonksiyonunun kodu da text bölümünde bulunur. Aksi belirtilmediği sürece bu bölümler gcc derleyicisi ve ld bağlayıcısı tarafından aşağıdaki sırada oluşturulmaktadır: Aslında bss ve stack bölümleri çalışabilen dosyanın içerisinde yer kaplamaz. Genellikle bu bölümlerin yalnızca uzunlukları çalışabilen dosya formatında tutulur. Yükleycici dosyayı sanal belleğe yüklediğinde bss ve stack aşağıda (yüksek anlamlı bölgede) kalmaktadır. bss bölümü yükleme sonrasında sıfırlanmaktadır (ilkdeğer verilmemiş yerel statik ömürlü nesnelerin içerisinde 0 olduğunu biliyorsunuz). Dosya sanal belleğe yüklendikten sonra aşağıdaki gibi bir görünüm elde edilir: 7 Kaan Aslan Makale Arşivi – www.kaanaslan.net Görüldüğü gibi heap alanı bss alanının hemen altından başlatılıp aşağıya doğru büyümektedir. Böylelikle bss ve stack alanlarının çalışabilen dosya içerisinde yer kaplaması engellenmiş olur. Düz ikili dosya üretilirken aksi belirtilmediği sürece yukarıdaki sıraya uygun kod üretilir. bss bölümünün sonda bırakılması düz ikili dosyada yer kaplamasını önlemektedir. Ancak tabi bu bölümdeki nesneler için offset sanki bu bölüm varmış gibi üretilir. 8 Kaan Aslan Makale Arşivi – www.kaanaslan.net