21 Ekim 2014 Salı

Exploitler, Korunma Teknikleri ve Karşı Teknikler


Yine son yazıdan bu yana epey zaman geçti. Yine konu olarak daldan dala atlayıp bütün yıl yazdıklarımdan ilgisiz bir konu ele alacağım. Konuyu bilimsel bir yayına bağlayacağım ama önce bir girizgah olacak. Yeterince zamanı olmayan okuyucu yalnızca son üç paragrafa göz atabilir. Aslında ne zamandır donanımsal birşeyler yazmak istiyorum ama sipariş ettiğim malzemeler henüz gelmediğinden birşeyler yapamıyorum.

Öncelikle, Internet Explorer (IE) kullanan var mı bilmiyorum ama ben Mozilla'dan sonra kesintisiz olarak kullanmayı bıraktım. En önemli neden daha kararlı çalışıyor ve daha az bug'ı olması. Mozilla'nın da azımsanamayacak bug'ı ve yamaları var ama yine de daha güvenli bazı teknikler (Flash nesnelerini yada JavaScript'leri çalıştırırken Sandbox'ta çalıştırması örneğin) kullanıyor. Bu bağlantıda Internet Explorer'ı kullanmamak için 7 neden sıralanıyor. Bu nedenlerden ilki ikisi ama önemlisi güvenlikle ilgili ki ABD iç güvenlik bakanlığı (homeland security -- nasıl Türkçeleştiriliyor bilmiyorum bu nedenle kendime göre çevirdim) IE kullanmayın diyor. Yazının tarihi biraz eski, büyük olasılıkla yama çıktı.

IE, özellikle 8. ve 9. sürümlerden itibaren güvenlik konusunda radikal değişiklikler yaptı yada yapmaya çalıştı. Örneğin IE8'de JavaScript'in ayırdığı bellek blokları arka arkayayken IE9'da rastgele oluyor. Üstelik IE8, eş zamanlı olarak işletim sistemine gömülü DEP (Data Execution Prevention) kullanıyor. Hoş, IE10'a kadar bu oldukça basitçe etkisiz hale getirilebiliyor. Bu tekniklerin temeli ROP zinciri (ROP chain) adlı yönteme dayanıyor.

En başta sözünü ettiğim açık, daha da karmaşık bir açık ve IE11 sürümüne kadar olan sürümleri etkiliyor. Üstelik ilk olarak açıklandığı zaman Microsoft, ancak olanları izlediğini, yazılımcıların bir yama üzerinde çalıştıklarını ve kullanıcıların açık kapatılıncaya kadar IE'de Adobe Flash'ı kapatmaları gerektiğini açıklamakla yetinmişti. Yamanın çıkması 5 gün sürdü. Açıkla ilgili ayrıntılı bilgi ve açığın shellcode'u bu bağlantıdan görülebilir.

Heap spraying ve ROP zincirlerini biraz yüzeysel anlatacağım çünkü ben de bunlara çok hakim olduğumu düşünmüyorum ama okuyucuya araştırması için bir görüş sağlayabileceğini düşündüm.

Heap spraying'in kendisi bir exploit tekniği değil. Heap spraying'te yalnızca bellekte bloklar ayırıp bunların içini kendi kötü amaçlı yazılımınızın makina kodlarıyla doldurursunuz. Bazı güvenlik yazılımları, çalışan yazılımların özellikle internet tarayıcılarının ve bunların içinde çalışan Flash, Adobe Reader, JavaScript vb. modüllerin bellekte ayırdığı blokları kontrol edip içlerinde 'alışılmadık' verilerin olup olmadığına bakarak sıfırıncı gün saldırılarından (zero-day attack) bile koruma sağlayabiliyor. Paragrafın başında da belirttiğim gibi heap spraying yalnızca makina kodlarının belleğe yerleştirilmesi için olanak verir. Başka bir teknikle bu kodlara yada yakın bir yere "Jump" yada "Ret" (ret: return. x86 assembly komutu) etmek gerekir.

[ Yazının ileriki altı paragrafında Corelan.be'deki Heap Spraying tutorial'ini temel aldım ve kısaltarak Türkçeleştirdim. Örnekleri bu sayfadan alacağım. Ayrıntılı bilgi için tutorial'in tamamı okunabilir. ]

JavaScript'de var myvar2 = new String("CORELAN!"); ifadesiyle bir değişken tanımlandığında bellekte unicode olarak yer alır. Sekiz karakter, sıfır sonlandırma karakteri ve unicode'dan ötürü 16 bit'ten hesaplandığında bellekte 18 byte yer kaplar. "Unescape" komutuyla unicode karakterlerin kodları girilerek (eskiden ALT'a basılı tutarak karakterin ASCII kodunu sayısal klavyeden girerdik, onun gibi) sayısal veriler doğrudan belleğe yerleştirilebilir. Örn: var myvar = unescape('%u\4F43%u\4552'); komutu bellekte unicode'suz olarak 'CORE' karakter katarını belleğe yerleştirir (Little endian'a dikkat).

Elimizde 256b'lık bir makina kodu olduğunu varsayalım. Yazılımsal kısıtlamalardan ötürü bunun tam olarak bellekte nereye yükleneceğini bilemiyoruz yada bilsek de tam başına dallanamıyoruz (örneğin bir buffer taşmasından yararlandığımızı düşünelim). Bir olasılık mümkün olan bütün belleği, bu 256b'lık bloklarla doldurmak olabilir ama dallanmanın bloğun ortasına bir yere olması durumunda kod düzgün çalışmayacaktır. Bunun yerine bu blokların başına sonuna bir sürü nop'lar yerleştirilir ve bloklar 4K-8K gibi olacak şekilde ayarlanır. Sonra bu bloklardan belleğe yüzlerce kopyalanır. Bellekte blokların ayrıldığı yerin yaklaşık bir noktasına dallanılınca bir nop'a düşme olasılığı fazladır ve bu nop'lar işletile işletile en sonunda çalıştırılmak istenen kod parçacığına sıra gelir. 3/4'ü su olan dünyada rastgele bir noktaya düşecek olan uzay kapsülünün suya yumuşak iniş yapma olasılığının 3/4 olmasına benzer bir durum. Bellek bloklarının arasında ayrılmamış (unallocated) bir bellek bölgesine yada kodun ortasında bir yerlere dallanma olasılığı düşüktür ancak böyle birşey gerçekleşse bile program çöker ve kapatılır.

Buraya kadar anlattıklarım aslında exploitin çalıştırılmasıyla değil yalnızca belleğe aktarılmasıyla ilgili. Yüklenen kodun çalıştırılması tampon bellek taşmasıyla (buffer overflow) hallediliyor. Kodun bir yerlerinde tampon bellek sınırları kontrol edilmiyorsa (bound check) tampon belleği taşıran bir girdinin taşan kısımları yığına (stack) denk getirilebilir. Nasıl yapılacağı wikipedia'da Buffer overflow maddesinde de açıklanıyor. Açıklamak kolay olduğu için yığın tabanlı taşmayı anlattım ancak IE'deki açığa bildiğim kadarıyla heap tabanlı bir taşma neden oluyor.

ROP zincirinin mantığı buna dayanıyorsa da biraz daha karmaşık. DEP yada başka bir koruma mekanizmasının doğrudan yığındaki bir adresten exploitin koduna dönüşü (ret) engellediğini varsayalım. ROP zinciri yaklaşımında, bu engellemeden kurtulmak için öncelikle kod içerisindeki 0C3h byte'ları incelenir. Kodun içinde operand olarak yer alan tüm 0C3h'ların (örn. mov eax, 0C3012368h = 0B8, 68h, 23h, 01h, 0C3h) yerleri ve bunlardan önce gelen kod parçaları listelenir ve yığın öyle değiştirilir (taşma ile) ki yığının en başındaki (ilk ret ile dönüş yapılacak) değer kodun içindeki bir 0C3h'dan bir kaç byte önceki bir yeri göstermektedir. Kod parçası ret'e kadar çalışır ve yığında 0C3h ile biten başka bir kod parçasına atlar. Bu 0C3h'lardan önce gelen değerler uygun biçimde sıralanıp birleştirildiğinde exploit yazanın istediği bir kod parçasını verir. Yukarıdaki örneğe dönersek, komut EAX'e 0C3012368h değerini yazmak istemektedir. İncelerken kolaylık olması için bunun 0000 0100h'e yerleştiğini varsayalım.

0000 0100 B8 68 23 01 C3 . . . - . . . . . . . 

Exploiti yazan kişi koddaki 0C3h'lara bakarken bunu görüyor ve başındaki kodu inceliyor:
1) 01h, 0C3h: add bx,ax . ret gitti işe yaramaz.
2) 23h, 01h, 0C3h: and ax,[bx+di] / ret. Bir kenarda dursun.
3) 68h, 23h, 01h, 0C3h: push 0123 / ret. İşte bu!

Exploiti yazan şahıs heap spray'i 0123h adresini kapsayacak şekilde yapabilmişse, ROP zinciri ilk halkasında heap spray yapılan yere dallanıp exploiti çalıştıracaktır. Elbette uzun bir kodda buna benzer kullanılabilecek birden fazla 0C3h byte'ı bulunacak ve bunlardan bazılarını uygun sıraya dizince exploit yazarının işini kolaylaştıracak. (Bu arada dikkatli okuyucu mov eax'in opkodunda 66h öneki olmadığından 32-bit olduğunu ancak push'un operand'ı 16-bit olduğu halde 67h öneki olmadığından 16-bit olduğunu farkedecektir. Evet burası hatalı ama umarım örneği anlatabilmişimdir.)

Exploitler genel olarak heap spray'i kullanıyorlar ve buna karşı geliştirilen bazı korunma teknikleri bulunuyor.
  1. Nozzle: Microsoft'un ürünü olan Nozzle, çalıştırılabilir kodlara benzeyen karakterler belleğe kopyalandığında bunları analiz ediyor ve izliyor. Çalışmasıyla ilgili ayrıntılı bilgi 38 sayfalık bir teknik raporda anlatılıyor. Her ne kadar yalnızca özetini okudumsa da küçük bir emülatör kullandığından bahsediliyor. Bu konuda akla gelebilecek diğer bir yaklaşım da makina öğrenmesi olabilir ancak bunu uygulamak daha zor. 
  2. BuBBle: Bu teknik heap spray algoritmasının bellekte birbirine benzeyen onlarca dizi açmasını izliyor. Heap spray için ne kadar çok yer ayrılır ve belleğe kopyası yerleştirilirse, exploitin başarılı olma olasılığı o denli yükselir. BuBBle ile bunun önüne geçiliyor.
  3. The Enhanced Mitigation Experience Toolkit (EMET): Exploitin shell kodundaki kısıtlamalar, yığında uygulanmasının kolay olması yada hem adres hem de opkod olarak karşılığı bulunmasından ötürü (nedeni Corelan.be'deki belgede anlatılıyor) heap spray'den sonra exploitler 0A0A0A0Ah, 0C0C0C0Ch gibi bellek adreslerine dallanmayı tercih ederler. EMET, bu adreslere heap spray yapılmasını engellemek için oraya kendi yerleşerek erişimi kısıtlıyor. Tam da Microsoft'a göre bir yaklaşım. Vista'nın SP1'inde geliyor. 
  4. HeapLocker: HeapLocker, yukarıdaki teknikleri ve bir kaç ek tekniği tek bir programda birleştiriyor. NOP sled denen, NOP komutunun bellekte büyük miktarda ardarda yer aldığı durumlarda kullanıcıyı uyarıyor. Heap spray için JavaScript'le unescape komutu sıklıkla kullanılır. HeapLocker'da JavaScript'le unescape gibi komutların çalıştırılması engellenebiliyor. Bu arada sayfasını incelerken farkettim ki XorSearch'ü de yazan Belçikalı ve fazlaca üretken bir adammış. Sertifikalarına bakınca kıskanmamak elde değil.
Gelelim madde madde bu tekniklerin nasıl kandırılabileceğine.

NOP dizileri artık eskisi gibi yalnızca 90h'lardan oluşmak zorunda değil. Intel belgelerinin [1] NOP maddesinde birden fazla byte uzunlukta NOP gerektiğinde bunları kullanın diyerek aşağıdaki tabloyu veriyor:

1 byte : 90h
2 byte : 66h 90h
3 byte : 0Fh 1Fh 00h
4 byte : 0Fh 1Fh 40h 00h
5 byte : 0Fh 1Fh 44h 00h 00h
6 byte : 66h 0Fh 1Fh 44h 00h 00h
7 byte : 0Fh 1Fh 80h 00h 00h 00h 00h
8 byte : 0Fh 1Fh 84h 00h 00h 00h 00h 00h
9 byte : 66h 0Fh 1Fh 84h 00h 00h 00h 00h 00h

Elbette bunlar belgelenmiş olduklarından artık bir çok yazılımın bunları tanıyabildiği varsayılabilir. NOP'un uzunluğu arttıkça dallanmanın, kodun ortasına denk gelme olasılığı artıyor. Bir de bazı opkodlar var ki bunlar aslında birşey yapsa da yaptığı işlem o kadar önemsiz ki bunlar da NOP kapsamına alınabilir. Örneğin 0Ch, 0Ch; or al, 0Ch'ya karşılık geliyor. Yaptığı iş oldukça önemsiz. 0Dh, 0Dh, 0Dh, 0Dh, 0Dh; or eax, 0D0D0D0Dh'ya karşılık geliyor. Yine yaptığı iş görmezden gelinebilir. Üstelik ardarda birden çok defa yapmanın zararı yok. Bu yaklaşımlarla NOP dizilerini inceleyen yazılımlar kandırılabilir. Komutun sonucu etkisiz de olsa yazılım komutun gerekli yada gereksiz olduğuna karar veremez.

Biribirine benzeyen bellek bloklarının saptanmasında yukarıdaki teknik yine yardımcı olabilir. JavaScript'le bir blok 0Ch'lerden, bir blok uzunluğu rastgele değişen NOP'lardan, bir blok 0Dh'lerden oluşabilir veya etkisiz opkodlardan bulunarak bunların kombinasyonu yapılabilir. 

Ve en önemlisini en sona sakladım. 5 yıl önce yayınlanan "English Shellcode" adında bir yayın. Baktığımda 33 yerde atıfta bulunulmuş. Çok ünlü değil ama az ünlü de sayılmaz. Yayının ana fikri normal metinlerin ASCII kod aralığına karşılık gelen ( [30h-7Fh] ) assembly kodlarının aslında exploit oluşturmaya yetecek olanak sağladığı. Üstelik bu işi rastgele harf karışımlarıyla değil İngilizce metinlerle de oluşturmuşlar (yayındaki Fig. 9). Yayında Fig. 10'un altındaki şu yazı can alıcı: "For comparison, we also “disassembled” 500 Wikipedia articles selected at random and 6 English shellocode samples". 

Yukarıda Nozzle'ın verileri analiz edip çalıştırılabilir olup olmadığına baktığını belirtmiştim. Eğer Nozzle bu veriyi (metni) çalıştırabilir kod olarak algılarsa bütün metinlerin aslında bir kod karşılığı olacağından Nozzle işlevsiz duruma geliyor. Algılamazsa -ki bence algılayamaz- o zaman Nozzle'ın işlevsizliği ortaya çıkıyor. Diyelim ki, ileri bir koruma mekanizması olarak JavaScript'in bellekte yalnız metin tabanlı içeriği yüklemesine izin versek bile (yani görüntü, video, pdf vb. verileri tümden kaldırıp attık) onun da aşılmayacağının garantisi olmadığını bu yayın kanıtlıyor. Üstelik yayında verilen örnek cümle, İngilizce'nin dizilim ve anlam kuralları (semantik) bakımından da doğru. Böyle bir kısıt olmasaydı, metin gibi görünen anlamsız kelimeler sıralamasından daha kolaylıkla bir shellcode çıkarılabilirdi. Üstelik belleğe yüklenen metnin anlamlı ve kurallı bir cümle mi yoksa İngilizce kelimelerin anlamsızca rastgele sıralandığı bir shellcode mu olduğunu ayırdetmesi için gerekli analizler Nozzle benzeri koruma mekanizmaları için bile oldukça ağır ve maliyetli. Deyim yerindeyse pire için yorgan yakmak durumunda. Üstelik yapılsa bile yazılımın performansı büyük olasılıkla yerlerde sürünecektir. 

Bir önceki paragrafın biraz bardağa boş tarafından bakmak olduğunun farkındayım. Sonuçta söz ettiğim yayın böyle bir olasılığın var olduğunu gösteriyorsa da bu şekilde yazılabilecek bir exploit oldukça sınırlı bir alanda çalışıyor olacak. Dolayısıyla Nozzle benzeri koruma mekanizmalarını böylesi bir olasılıkla yetersiz kalabileceği için kaldırıp atmak mantıklı olmaz ama kodlama açısından bu şekilde yazılacak bir exploit bence günümüz için gelinen son nokta olacaktır.


[1]: Intel 64 and IA-32 Architectures Software Developer’s Manual

3 Temmuz 2014 Perşembe

Türkiye İlleri ve Plakaları Üzerine Gereksiz Bir İnceleme


Bir önceki (yada iki herneyse) herhangi birşey yazmadığın 6-8 aylık zaman zarfında gereksiz işlerle uğraştığımı yazmıştım. Bu yazıda biraz bundan bahsedeceğim. Yayınlayacağım bazı veriler ve bunların yorumları anlamsız gelebilir. Örneğin plaka numaraları. Ancak bütün bu iş plaka numalarından çıktı. Herşeyi başlatan soru şuydu: "Karadeniz bölgesinde, özellikle Doğu Karadeniz'deki plakalar 50 ile 60 arasında öbeklenmiş gibi. Gerçekten de matematiksel açıdan böyle bir öbeklenmeden somut bir biçimde söz edilebilir mi yoksa bu yalnızca bana mı öyle geliyor?" Böyle bir kanıda olmamın temel nedeni şu iller: Ordu (52), Rize (53), Samsun (55), Sinop (57), Tokat (60), Trabzon (61) ve ucundan biraz da bölgeye yakın olmasından dolayı Sivas (58).

Haritayı ve verileri yorumlarken biraz istatistik biraz da çizge (graf) teori kullanacağım. Çizge teorisindeki temel kavramları okuyucunun bildiğini varsayarak bunların tanımlarını vermeyeceğim. Bilmeyen ama öğrenmek isteyenler için bu öğelerin Wikipedia bağlantılarını sunacağım. Öncelikle belirtmeliyim ki, benim yaptığıma benzer bir biçimde il merkezlerini çizgenin düğümleri (vertex) ve aralarındaki komşulukları da bu düğümler arası kenarlar (edge) olarak alan Kaan Öztürk'e ait bir çalışma var. Güzel bir çalışma ancak benim açımdan eksiği, iller arasındaki kenarlar oluşturulurken iller arası mesafelerin (kuş uçuşu yada karayolu) hesaba katılmamış olması. Demek istediğim ben bu çalışmada Konya'nın Ankara'ya olan komşuluğu ile İzmir'in Manisa'ya olan komşuluğu arasında fark olduğunu varsayacağım (birinin merkezleri arası 258 km, diğerinin 35km). Öbeklenme olup olmadığını deneyebilmem için bunu da hesaba katmak zorundayım. Bir de bazı tartışmalı noktalar var. Örneğin illerin birbirlerine olan sınır komşuluklarını araştırırken K. Öztürk'ün çalışmasından başka Harita Genel Komutanlığı'nın sitesinden de yararlandım. HGK'dan bakıldığında İstanbul'a bakıldığında sınır olarak Kırklareli'yle arasında Tekirdağ'ın çok ince bir parçası bulunuyor (K. Öztürk, İstanbul ve Kırklareli'ni komşu olarak alıyor). Ben HGK'daki bilgileri esas aldım. Bir tek HGK sayfasında Kilis'in Hatay'la komşuluğu açıkça görünmüyor. Bunların komşu olmadıklarını varsaydım.

Çizge teorisi tabanlı derin bir çözümlemeye girmeden önce basit istatistiksel verilerle bir öbeklenmenin olup olmadığına baktım. En başta sorduğum soru Karadeniz Bölgesi'ni temel aldığından bölgelerin analiziyle başladım. Coğrafi ve beşeri veriler için temel olarak Wikipedia'yı kullandım, dolayısıyla oradaki bir hata analizlerime yansımış olabilir ancak veriler bölgeler kapsamında olduğundan çok büyük bir yanlışlığa yol açabileceğini düşünmüyorum (başka bir deyişle bağıl hata küçük). Bölge sınırları illeri tam olarak kapsamıyor. Örneğin Karaman, Akdeniz Bölgesi'nde mi yoksa İç Anadolu mu? Veya Çorum, Karadeniz Bölgesi mi yoksa İç Anadolu mu? Bu durumlarda ilçe sınırlarının bölge sınırlarıyla çakıştığını varsayıp, bir ilin hangi bölgede daha fazla nüfusu varsa o ili o bölgede saydım. Bu tür durumlar sıklıkla Akdeniz Bölgesi'nde olduğundan oradan bir örnek vereyim. Örneğin Denizli'nin Acıpayam, Çameli, Bozkurt ve Beyağaç ilçeleri Wikipedia'ya göre Akdeniz Bölgesi'nde yer alırken diğer ilçeler Ege Bölgesi'nde. Denizli'nin ilçelerinin nüfusuna baktığımız zaman il merkezinin diğer bütün ilçelerden fazla nüfusu var. İl merkezi Ege'de olduğuna göre isterse diğer bütün ilçeler Akdeniz Bölgesi'nde olsun, şehri Ege bölgesi'nde değerlendirdim. Parantez içerisinde, bu tabloda dikkati çeken başka bir konu da Denizli nüfusunun 1990 hatta 2000'e kadar ilçelerde daha fazla olup 2000'den sonra il merkezinin ilçeleri geçtiği. Bunun Pamukkale Üniversitesi'nin açılışıyla ilgisi olabilir.

İllerin plakalarını bu kritere göre değerlendirip, ilgili bölge sınırlarında kalan illerin plakalarının ortalama (μ) ve standart sapmalarını (σ) hesapladım ve aşağıdaki haritayı elde ettim. (Ortalama ve standart sapmayla ilgili: [1], [2], [3], [4], [5] ve [6]).

Bölgelere göre plakaların ortalama ve standart sapması
 
Haritada eşitliğin karşısındaki birinci sayılar 81 ile göre olan değerler. Parantez içerisindeki sayılarsa 67 ile göre yapılan hesaplamanın sonuçları. Bu haritadan çıkarılacak sonuç, genel olarak standart sapmada bölgeden bölgeye kayda değer değişiklik yok. Elbette ki plakalar yaklaşık olarak tekdüze giderken yeni illerin oluşturulmasıyla birden bire varolan tekdüzelik yerini gürültü olarak adlandırılabilecek noktalara bırakıyor. Bu da standart sapmada artışla kendini gösteriyor. Sonradan eklenen illerin hepsi sona eklendiğinden ortalamanın büyümesi beklenen bir durum. Güneydoğu Anadolu Bölgesi'ndeki ortalamanın yüksek olması orada çok sayıda yeni il bulunmasına bağlı. Başka bir dikkat çekici durumsa, Ege Bölgesi'nde hiç yeni il oluşturulmaması.

Yukarıda bahsettiğim ve varolduğunu varsaydığım tekdüzelik aşağıdaki haritayla görselleştirilebilir:
Gruplandırılacak görüntü
Bu harita illerin, plaka kodlarına göre siyah beyaz skalada renklendirilmesiyle oluşturuldu. 81 il için plaka kodu 3 ile çarpılarak gri kodu hesaplandı. Örneğin Aydın'ın RGB kodu #1B1B1B (Onluk tabanda 27 27 27). Bu haritaya bakıldığında belli bir öbeklenme görülebiliyor mu? Bence var. En başta varolduğunu düşündüğüm Trabzon-Rize öbeği Giresun (28) ile kesiliyor olsa da ondan daha büyük bir öbek göze çarpıyor. Sinop (57), Samsun (55), Ordu (52), Tokat (60), Sivas (58)'tan oluşan bir öbek. Ve buna çevre birkaç il daha katılabilir.

Bu analizler gözönüne alındığında somut bir veri vermiyor. Göz kararı öbeklenebilir görünüyor ama istatistik tam tersini söylüyor. Bu nedenle daha derin inceleme gerekiyor. Kullanacağım bilimsel yöntem, yüksek lisans tezi zamanlarımda rüyamda bile görmeye başladığım bir yayını temel alıyor: "Normalized Cuts and Image Segmentation". İlgili olanlar makaleyi okuyabilir. Ben temel noktaları anlatıp geçeceğim. Öncelikle haritayı bir çizge olarak düşünmek gerek. K. Öztürk'ün çalışmasında da yapıldığı üzere iller, çizge düğümleri ve aralarındaki komşuluk ilişkileri de çizgenin kenarları olmak üzere Türkiye haritasını göstereceğim. Çizgelerin matematiksel olarak temsilinde komşuluk matrisi adı verilen matristen yararlanılır. Basit bir veri yapısı olan komşuluk matrisinde, matrisin büyüklüğü çizgenin düğüm sayısıyla aynıdır. Türkiye haritası için bu, 81 x 81'lik bir matris olacak. Bu matris üç farklı şekilde doldurulabilir. Birinci satır ve sütun Adana, ikinci satır ve sütun Adıyaman vb. olacak şekilde;
  1. Eğer illerin yalnız komşuluklarıyla ilgileniyorsam komşu olan illere karşılık gelen satır ve sutunu 1, geri kalan öğeleri sıfır yaparak temsil edebilirim. Yazının en başında bağlantısını verdiğim K. Öztürk'e ait çalışmadaki yaklaşıma karşılık gelen temsil budur.
  2. Eğer illerin tümüyle ilgileneceksem aralarındaki kuş uçuşu uzaklığı il merkezlerinin koordinatlarından hesaplayabilirim yada daha iyisi karayollarının sayfasından iller arası uzaklık cetvelini indiririm. Bu cetvel karayolları haritasının kenarındaki üçgen tablo aslında.
  3. Ve eğer bütün illerle uğraşmak istemiyorsam yukarıdaki iki yaklaşımı karıştırırım: Birinci ve ikinci maddedeki matrisleri öğe öğe çarpıp (hadamard çarpımı) sadece birbirine komşu illerin aralarındaki uzaklığı dikkate aldığım bir matris elde ederim. 
Komşuluk Matrisi
Mavi noktalar sıfırdan farklı değerler olmak üzere, (1) ve (3) maddelerindeki matrisin genel görüntüsü yandaki gibidir.
Uzaklıkları dikkate aldım çünkü birbirine yakın olan illerin plaka numarası da birbirlerine yakınsa bunları gruplandıracağım. Ama işin teorisiyle ilgili açıklayacağım bir kaç nokta daha var. Düğümlerin aralarındaki ilişkilere (yani kenarlara) katsayı atanmasına yazında ağırlıklandırılmış çizge deniyor [7].  Örneğin bir ilden diğerine en kısa yoldan gitmek istiyorsam, ikinci maddede anlattığım çizge üzerinde en kısa yol problemini çözmem gerekir. Ağırlıklandırılmış çizgeler için bir çok örnek bulunabilir.

Komşuluk matrisiyle ilgili önemli nokta, bu problemde komşuluk matrisinin simetrik olması. Bunun anlamı; Samsun (55), Sinop'la (57) komşuysa matrisin 55. satır ve 57. sütununa karşılık gelen değer 1 olacaktır (ağırlıklandırılmamış çizgeden bahsediyorum). Elbette ki Sinop da Samsun'la komşudur ve 57. satır 55. sütuna karşılık gelen değer de 1 olacaktır. Veya Sinop'tan Samsun'a 163km yol varsa, Samsun'dan Sinop'a da aynı yol vardır. Bunu bütün iller için genelleştirebiliriz ancak bu şekilde olmayan yönlü çizgeler de bulunur. Samsun-Sinop yolunun tek bir tarafı çalışma dolayısıyla kapalı olsun. Samsun'dan Sinop'a gidebiliyoruz ancak geri dönerken Çorum-Amasya yolunu kullanmamız gerekiyor diyelim. Bu durumda artık matematiksel olarak birbirlerine bağlantıları olmayacağından komşuluk matrisi simetrik olmayacaktır. Veya örneğin, Adana'dan Kayseri'ye otobüsle 40 TL'ye gidebiliyorum ama dönüşte 50 TL ödüyorum. Adana'dan Niğde'ye 25 TL ve Niğde'den Kayseri'ye 20 TL ödeyerek gidebildiğimi varsayalım. Bu durumda giderken Adana'dan Kayseri'ye daha ucuza geçerim ama dönüşte Niğde üzerinden dönersem daha ucuz olur. Buradaki örnekte her iki yönde düğümler arasında farklı ağırlıklar vardır.

Komşuluk matrisi üzerine biraz daha oynayabiliriz. Bu matrisin satır yada sütun toplamlarını incelersek bize en az ve en çok komşusu olan iller hakkında bilgi verir. Buna göre sadece Kilis'in bir komşusu var. Hakkari, İstanbul, Kırklareli, Iğdır ve Yalova, iki komşuyla ondan sonra geliyor. En fazla sayıda komşusu olan iller 9 komşuyla Erzincan, Erzurum ve Konya. Bunlardan sonra 8 komşuyla Bolu, Diyarbakır, Sivas ve Yozgat geliyor.

Uzaklıklar matrisinin toplamları en merkezi ve en az merkezi olan iller hakkında bilgi verir. Buna göre en merkezi 5 il sırasıyla Kırıkkale, Kayseri, Kırşehir, Yozgat ve Nevşehir. İç Anadolu'da toplanmalarına şaşırmamak gerek. En az merkezi il olarak Hakkari açık ara ileride (bütün illerle mesafeleri toplamı 91 179km). Sonra sırayla, Edirne, Van, Kırklareli ve Iğdır geliyor. Bunların mesafeleri toplamı 82 000 ile 80 000km arasında değişiyor.

Sadece komşu illerin uzaklıkları matrisi yerel bilgi sağlar, çok fazla bilgi sağlamaz. İlerleyen paragraflarda anlatacağım konuya örnek olması için bunu da inceleyelim. İnceleyeceğim toplamlar illerin komşularına uzaklıkları toplamı. Bütün komşularına en yakın olan, sadece tek komşusu olan Kilis. En uzak olan, hem büyüklüğünden hem de çok komşusu olduğundan Konya. Fakat bu güvenilir bir bilgi değil. Bunun yerine uzaklık toplamlarını komşu sayısına bölüp ortalamaya baksak, yani komşu sayısından bağımsız olarak uzaklıkları normalleştirsek daha ilginç veriler sunuyor. Böyle yapınca en düşük yine Kilis ama komşularına ortalamada en uzak il bu sefer Antalya çıkıyor. Mersin'den sonra Konya ancak üçüncü oluyor.

Komşuluk matrisleri üzerinden bir çok ilginç veri almak olanaklı. Komşuluk matrisindeki sıfırdan farklı değerlerin ilin komşularını gösterdiği belirtilmişti. Komşuluk matrisinin karesi alınırsa her ile karşılık gelen satırdaki sıfırdan farklı değerler o ilin komşuları ve komşularının komşuları yani ikinci dereceden komşularını verir. Küpü, üçüncü derece komşuları vb. Toplam komşuluk sayısı 394. Karesi alınınca birinci ve ikinci derece komşuluklar toplamı 1217, küpü alınınca 2219. 81 x 81'lik bir matriste 6561 eleman bulunur. Sıfırdan farklı eleman sayısı, komşuluk matrisinin 14. kuvveti alınırsa 6561 oluyor. Yani Türkiye'nin iki en uzak ili arasında 13 il bulunur.

Şimdi tekrar Shi ve Malik'in yayınına geri dönelim. Burada yapılmak istenen nokta (düğüm) kümelerini bölütlemekti (segmentation). Çizgelerde bazı komşulukların kaldırılarak belli özellikteki düğümlerin ayrıklaştırılmasına kesim adı verilir. Her bir görüntü öğesi (piksel) üst, alt, sağ ve sol komşuluğu olan bir düğüm olarak ele alınsın. Kenarlar görüntü öğelerinin aralarındaki ilişkilere (örn. s/b görüntü için renk farkları) göre ağırlıklandırılırsa, ağırlıklarının toplamları en büyük olacak kenarları bulup bunları silerek yapılacak bir kesim bir görüntüyü bölütlendirebilir. En büyük yada en küçük değerli kesimi bulmak çizge teorisinde önemli yeri olan bir konudur.


Örneğin; yandaki görüntüde siyah parça içindeki noktalar 0 ve beyaz parça içindeki noktalar da hep 255 değerindedir. Kenar üzerindeki noktalar düşünülürse bir yanında 0 diğer yanında 255 olan noktalar vardır. Kenar ağırlıkları kolaylık açısından renk farkları olarak alınsın. O zaman yalnızca siyah parçanın sınırlarında 255 değerinde olan kenarlar bulunur, görüntünün geri kalanında kenar ağırlıkları 0'dır. Bu görüntünün en büyük kesimi değeri 255 olan kenarların kesip çıkarılmasıyla elde edilir. Böyle yaparak siyah bölüm beyaz bölümden ayrılmış olur. Başka bir yaklaşım renk değerlerinin farkı arttıkça değeri azalan bir fonksiyon bulup (örn, x ve y renk değerleri olmak üzere f(x, y) = 1/(x-y) ve x = y için yeterince büyük pozitif bir sayı. mesela 2) en küçük kesimi bulmak olabilir. Ancak her iki yaklaşımda da bazı eksiklikler vardır. Görüntünün kenarında daha küçük başka bir siyah parça olsun. En küçük kesimi arıyorsak, algoritma alanı küçük olan parçayı seçmeyi avantajlı bulur (örneği tekrar hatırlayalım, Konya'nın çok komşusu olduğundan komşuları arası mesafe en fazladır ancak ortalamaya vurulduğu zaman çizgenin en ayrık noktası Konya değil Antalya'dır). Bu nedenle Shi ve Malik'in makalesinde kesilecek bölüm çizgenin tümüyle ilişkilendirilerek, normalleştirilmiş kesim adı verilen bir kesim tanımlanmıştır. Yapılan iş aslında yukarıda yapıldığı gibi sadece uzaklıklara değil, komşu sayısına göre ortalama uzaklıklara bakılmasıdır. Bu nedenle yukarıda normalleştirmenin altını çizdim. Daha sonra makalede normalleştirilmiş kesim bulmanın, komşuluk matrisini bazı işlemlerden geçirip ortaya çıkan matrisin özdeğer ve özvektör çiftlerinin bulunmasıyla aynı iş olduğu gösterilir. Makalede görüntüler üzerinde çalışılmıştır ama haritalara da uygulanabilir.

Makalede uzaklıklar ve renk değerlerinden kenar ağırlıkları belirlenerek bir komşuluk matrisi oluşturuluyor. Bu uygulamada ağırlık fonksiyonunu makalede verilen üstel fonksiyon gibi aldım (s. 7). Basitlik açısından sigma olarak gösterilen parametreleri hesaba katmadım. Harita için renk değerleri (intensity, F(i)'ler) plaka numaraları ve uzaklıklar (X(i)'ler) karayolu uzaklıkları oldu. Plakaları matrisini oluşturmak için kod;

for k1 = 1:81
    for k2 = 1:81
        plakalar(k1, k2) = abs(k1 - k2);
    end
end


Uzaklıklar Excel tablosundan, xlsread fonksiyonuyla Matlab'de okunabilir. Yazının başında saydığım gibi üç ayrı kriterden, komşuluk ilişkileri aynı ancak kenar ağırlıkları farklı üç ayrı çizge oluşacak:
  1. Yalnızca illerin komşuluk ilişkileri dikkate alınacaksa: K = exp(-plakalar) .* exp(-komsuluk);
  2. Bütün illerin kendi aralarındaki uzaklıklar dikkate alınacaksa: K = exp(-plakalar) .* exp(-uzakliklar);
  3. Sadece komşu iller arasındaki uzaklıklar dikkate alınacaksa: K = exp(-plakalar) .* exp(-(komsuluk .* uzakliklar));
K matrisi oluşturulduktan sonra bu matrisin satırları tek tek toplanır, 81 tane toplam elde edilir ve köşegen öğeleri bu toplamlar olan bir matristen, K matrisinin kendisi çıkarılır. Buna L diyelim. L'nin özdeğerleri ayrıştırılıp en küçükten bir büyük özdeğerine karşılık gelen özvektörü bulunur. En küçük olan, çizgenin kendisini gösteren özvektör olarak düşünülebilir. Bu bir bilgi içermez. İkinci en küçük özvektörün pozitif elemanlarıyla negatif elemanları bir öbek bilgisi içerir. Örneğin özvektörün yalnızca 2., 4. ve 6. elemanları pozitif, geri kalanları negatifse Adıyaman, Ağrı ve Ankara bir öbek ve geri kalanlar diğer öbeği oluşturmuştur. İstenirse pozitif yada negatif kısımlar ayrılıp, L matrisinin pozitif (yada negatif) elemanlarla bir permütasyonu alınır ve oluşacak yeni matrisin de ikinci en küçük özdeğerine karşılık gelen özvektörü bulunursa ikinci bir bölütleme yapılabilir.



Birinci yaklaşımda yazının başında bahsettiğim Sinop, Samsun, Sivas'tan başlayıp Konya'ya doğru giden bir öbek görünüyor. Bunlara Rize ve Trabzon da giriyor. İkinci mertebe bölütlemede yalnızca iki anlamlı sonuç var. Birincisi Sinop, Samsun öbeğinden çok da yakın olmayan 70'li plakalar çıkıyor (Kırıkkale, Karaman, Düzce, Bartın) ama öbeğin kendisi hala bütün. Diğer taraftaysa 20 ile 40 arası plakalı iller kendi aralarında serpiştirilmiş duruyor. Bu bölütlemedeki tek gerçek öbek Kars, Erzurum, Erzincan, Gümüşhane, Giresun ve onlarla birlikte az yakınındaki Elazığ, Diyarbakır öbeği bulunuyor. Daha ileriki bölütlemelerde (aşağıda) Samsun, Sinop ve Sivas'ın aralarında bulunduğu parça da Konya, Niğde parçasından kopuyor çünkü alfabetik olarak S'ler birbirine daha yakınlar. Dolayısıyla plakalarda da daha az fark var. Diğer yandaysa 20-40 arası olan öbek yaklaşık olarak 20-30 ve 30-40 arası olmak üzere iki ayrı öbek oluyor. Gümüşhane (29) bir öbekteyken Giresun (28), Erzurum (25) ve Erzincan'la (24) başka bir öbekte bulunuyor. Aslında dikkatle incelendiğinde öbekleme yapılırken komşuluktan daha çok ağırlık plakalara verilmiş gibi.
Birinci yaklaşımda üçüncü mertebeden bazı sonuçlar



İkinci yaklaşımın sonuçları

İkinci yaklaşımda birincisine göre daha anlamlı sonuçlar beklerken daha az anlamlı sonuç çıkıyor. Bunun nedeni, plaka komşuluğunun öbek oluşturmada uzaklıklardan daha az ağırlığı olması. Plaka komşuluk matrisi 0 ile 80 arasında değerlere sahipken uzaklıklarda bu değer birkaç yüz km'den 2000km'ye kadar değişiyor. Yayındaki sigma parametresi belki de her iki kıstasın ağırlıklarını eşitlemek için gerekiyordu. Sonuçta yandaki gibi bir harita çıkıyor. Örneğin alt tarafta doğuda Kars (36), Erzurum (25) gibi illerin öbeklenmesi bir dereceye kadar mantıklı gibi görünse de araya Şanlıurfa'nın (63) nasıl karıştığını plaka numaralarıyla açıklamak zor. Daha önceki Sinop, Samsun, Sivas ve diğer iller öbeği Sinop'suz olarak bulunuyor ama 50'li plakası olan illerin öbeğinde Kayseri (38) ve hatta Hatay (31) da yer alıyor. Bu yaklaşımın formülünde bir hata olduğu için tutarsız sonuçlar bulduğumu düşünyorum. Belki formüle bir ince ayar çekmek gerekebilir.

Son olarak üçüncü yaklaşımda yine birinciye benzer olarak sonuçlar var. Sanki bölütleme sadece plakalara göre yapılmış, komşuluk ilişkileri daha az dikkate alınmış gibi duruyor. Neredeyse ilk kırk bir bölümde ikinci kırk ayrı bir bölümde toplanmış. Daha sonraki mertebede ilk kırktan ilk yirmi de ikinci yirmi, sonraki kırktan da kırkla altmış arası ve son yirmi kalmış gibi. Sayısal sınırlar tam kesin olmasa da ayrımın hemen hemen böyle olduğu açık. Eğer bir başarıdan söz edilecekse bu da en fazla ilk yaklaşım kadar başarılı.


27 Mayıs 2014 Salı

Firefox 29 Güncellemesinden Sonra Eski Eklentilere Yeniden Kavuşun


Firefox 29 (Kaynak)
Yakın zaman önce Firefox'un 29. sürümü çıktı. Firefox, otomatik güncelleştirme seçeneği zaten açık olarak geliyor ve internetle çok fazla haşır neşir bir program olduğundan otomatik güncelleştirmenin her zaman açık olması bir yandan da güvenlik açısından vazgeçilmez bir durum. Diğer taraftan Mozilla, bu son sürümde Firefox'un görünümünü Australis adında bir "theme" kullanarak bir parça değiştirdi ve eski eklentilerin bazıları bu güncellemeyle kullanılmaz hale geldi. Bunun başlıcası da eski eklenti çubuğu (Add-on Bar). Eklenti çubuğunun yeni hali bize ne getirir ne getirmez bilmiyorum ama eskiden eklenti çubuğunda yer alan uygulamalar artık buraya yerleşemiyor. Buradaki uygulamaların yeni sürümleri çıkmadıysa bir çoğu şu an byte çöpü durumunda. Peki güncellenmeyen, geliştirilmesi durmuş yada acilen ihtiyacımız olan eklentilere ne olacak, işte bunun çözümünü anlatacağım.

Öncelikle belirteyim ki burada anlatacağım yöntemin her uygulama için çözüm olma garantisi olmadığı gibi çalışıp çalışmaması da kullandığınız eski eklentinin yapısıyla ilgili. 

Herneyse, Firefox 29 kurulduktan sonra bu sürümle uyumlu olmayan bütün eklentileri kapatıyor ancak eski sürümlerde bunlar elle tekrar açılabiliyordu. Şimdi bunu kaldırmışlar. İlk iş olarak Google'a "force firefox add on compatibility" diye aratıp çıkan sonuçlara baktım. Bu anlatacağım çalışmadı ancak eski sürümlerde çalışıyor olması lazım. Sıkça karşıma çıkan çözüm şurada anlatıldığı gibi Firefox'un registry'si olan about:config'e girip, burada extensions.checkCompatibility.x.y adında bir ikili anahtar (boolean key) oluşturup değerini "false" olarak vermek. x.y, uygulamaların denetleneceği minimum Firefox sürümü oluyor. Örneğin uygulamaların uyumluluğunu 13.0 sürümüne göre denetlenmesi istenirse oluşturulacak anahtar extensions.checkCompatibility.13.0 ve değerinin de "false" olması gerekiyor. Daha sonra her ihtimale karşı bir de Firefox'u tekrar başlatıyorsunuz. Fakat paragrafın başında da söylediğim gibi 29. sürümde bu şekilde çalışmıyor.

Asıl çözüme gelmeden önce bazı önkoşullardan bahsedeceğim. Normalde 64bit işletim sistemi kullanıyorum ve bu nedenle kullandığım bütün yazılımların da 64bitlik olmasına özen gösteriyorum. Diğer taraftan da hiçbir zaman alfa yada beta sürüm kullanmam. Kullandığım yazılımların mutlaka "stable" olan denenmiş sürümlerini hatta güvenlik açıkları yoksa "stable"dan da bir eski sürümlerini tercih ederim. Bu nedenle Firefox beni bir ikileme soktu. Anladığım kadarıyla Firefox 64bit'i yalnızca Nightly sürümlerinde destekliyor ancak bunlar için stabilite garantisi vermiyor. Stabil Firefox'ların tamamı 32bit. Bu nedenle ben Nightly değil stabil ama 32bitlik sürümü tercih ettim. Çalışan Firefox'un kaç bitlik olduğuna Görev Yöneticisi'nden bakılabilir.

Bunu anlattım çünkü eklenti uyumluluğunu kapatmak için "Nightly Tester Tools" adında bir eklenti kullandım. İlk başta adına aldanıp Nightly için bu, benimki 32bit dememe rağmen daha sonradan başka bir çözüm bulamayınca yükledim. Sorunsuz çalıştı. Bu eklentiyi kullanarak uyumsuz olduğu söylenen eklentileri çalıştırmak olanaklı. Nighty'i yükledikten sonra


"Force Addon Compatibility"yi açınca eski eklentilerinize kavuşuyorsunuz. Yada en azından artık uyumsuzluk nedeniyle pasifleştirilmiş eklentileri açabiliyorsunuz diyelim. Bu eklentiyi bir forum sitesinde gördüm. Forum başlığının ileriki mesajlarında Nightly Tester Tools'dan farklı bir eklentiden daha bahsedilmiş. Ben Nightly'i denedim sorun olmadı.

Buraya kadar eski eklentiyi sorunsuz çalıştırabildiyseniz sorun yok ancak yukarıdaki ekran görüntüsünden de görüleceği üzere örneğin WeatherForecast eklentisi eklenti çubuğuna yerleşiyor ve sadece WeatherForecast'i aktif hale getirince sorun çözülmüyor çünkü daha önce de bahsettiğim gibi artık gördüğünüz eklentiler çubuğu eski eklentiler çubuğu değil, öyle görünse bile. Mozilla'nın kendi sitesinde eklenti çubuğunu değiştirdiklerini kendileri de yazmışlar zaten. Eski eklenti çubuğunu geri getirmek için "The Addon Bar (restored)" adındaki bir eklenti daha gerekli. Bunu da Eklentilerin bulunduğu siteden yükleyince örneğin WeatherForecast eklentisi gibi eski eklenti çubuğunu kullanan eklentiler sorunsuz çalışmaya başladı.

Buraya kadar herşey tamamsa bir ince ayar kalıyor. Eğer yeni Firefox'ların garip pencere ve sekme görüntülerinden zerre hoşlanmıyorsanız üçüncü sürüm Firefox gibi görünmesini sağlamak için iki tane daha eklenti var. Birisi "Classic Theme Restorer". Bununla sekmelerin görüntülerini eski güzel görünümlerine geri döndürmek olanaklı. Bu eklentiyi yükledikten sonra Araçlar menüsü altında kendi kullanıcı arabirimine ulaşılabiliyor. En üstteki Firefox görüntüsünün kaynağında (yani şurada) bu eklentiyle ilgili İngilizce bir kaynak daha var. Diğer ikinci eklenti de "Classic Toolbar Buttons", eski sürüm Firefox düğmelerini kullanabilmek için. Bunun ayarları da eklentiler sayfasından (about:addons) eklentinin kendi ayar sayfasından yapılıyor. 

Elbette görünüm için iki ayrı eklenti kullanmak en azından benim için, ayarlar içerisinde kaybolmaya neden olabiliyor. Daha önceden Firefox için "Classic Compact" kullanıyordum ancak bu eklenti de Firefox 29. sürümle uyumlu olmadığından Firefox tarafından kapatıldı. Yukarıda anlattığım yöntemle bu eklentiyi de açtığım halde eklenti çalışıyor görünmesine rağmen yaptığım değişiklikleri uygulamadı. Bu nedenle Classic Compact yerine artık diğer ikisini kullanıyorum.

16 Mayıs 2014 Cuma

Unrar Kütüphanesiyle Paralel Şifre Bulucu #3


Herşeyden önce Soma'daki kömür madeni kazasından ötürü herkese baş sağlığı dileyerek başlıyorum.

Birşeyler yazmayalı ve yayınlamayalı çok uzun zaman olmuştu. Bu zaman zarfında, bilgisayarı yenileyince başta Euro Truck Simulator gibi her erkeğin hayali olan saçma sapan oyunlar yada eski bilgisayarımda çalışmayan ve uzun zamandır oynamak istediğim oyunlardan dolayı hemen hiçbirşey yapmadığım gibi yazmaya da zaman ayırmadım. O nedenle neredeyse üzerinden bir yıl geçecek olan yazı dizisine hiçbirşey olmamış gibi devam ediyorum. Bir de 4-5 hafta sonrasına yazılması gereken bir rapor var, onu yazmamak için blog yazayım dedim.

Önce seri sürümünün kodunu açıklayacağım. Daha önceki yazılarda da söyledim, benim yazdığım seri sürümden çok daha iyi (ve hızlı) sürümler internette de bulunabilir. Benim yaptığım tek yenilik sözlük dosyasını bilgisayarlar arasında bölüp işlemi paralelleştirmek oldu.

Kodla ilgili açıklamaları yine kodun içerisinde açıklama satırlarında yaptım. Bölük pörçük olmaması için kodu bölen başka bir açıklama girmeyeceğim. Uzun yorum satırları bölünmesin diye karakter boyutunu küçülttüm. İnceleyecek olan zaten kodu Notepad++ yada gedit gibi bir programda inceleyebilirse daha iyi olur.


#include<stdio.h>
#include<stdlib.h>
#include<string.h>

// unrar kutuphanesine ait header dosyalari
#include<raros.hpp>
#include<dll.hpp>


void DosyaCoz(char *arsiv, int sira)    {
    // dosya adlari acik, dosya sifreliyse cozme islemi
    HANDLE PASCAL hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;    // RAR dosyayla ilgili veriler
    struct RARHeaderData *Baslik;    // RAR icerisindeki dosyalarla ilgili veriler
    char sozluk[] = "sozluk.txt";    // sozluk.txt denenecek kelimelerin oldugu dosya
    char kelime[256];
    FILE *hDosya;
    int status, k1, k2 = 0;

    hDosya = fopen(sozluk, "r");

    AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));
    Baslik     = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));

    while(!feof(hDosya))        {
    // sozlukten bir kelime oku
        fscanf(hDosya, "%s", kelime);

    // RAR kutuphanesinin dosya acmak icin kullandigi yapi
        AcmaVerisi->ArcName    = arsiv;
        AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
        AcmaVerisi->CmtBuf     = NULL;
        AcmaVerisi->CmtBufSize = 0;

    // RAR dosyasini ac.
        hArsiv = RAROpenArchive(AcmaVerisi);
        Baslik->CmtBuf   = NULL;

        for(k1 = 0; k1 < (sira - 1); k1++)    {
            // ilk sifreli dosyaya kadar oku, yalnizca bazi dosyalar sifrelenmis
            // olabilir.
            RARReadHeader(hArsiv, Baslik);
            RARProcessFile(hArsiv, RAR_SKIP, NULL, NULL);
        }

        // ilk sifreli dosyayi oku
        RARReadHeader(hArsiv, Baslik);
        RARSetPassword(hArsiv, kelime);

        // sifreyle birlikte dosyayi test et. eger sifir dondururse sifre dogrudur.
        status = RARProcessFile(hArsiv, RAR_TEST, NULL, NULL);
        fprintf(stdout, "%8d", ++k2);
        fflush(stdout);

        if(status == 0)    break;  // dogru sifre bulunduysa donguyu sonlandir.

        RARCloseArchive(hArsiv);
    }

    // sozluk.txt'nin sonuna gelinmemisse (break ile cikildiysa) sifre bulunmustur
    if(!feof(hDosya))
        // sifre bulunduysa ekrana yaz
        printf("Sifre: %s\n", kelime);
    else
        printf("Sifre bulunamadi.\n\n");

    fclose(hDosya);

    return;

}


void ArsivCoz(char *arsiv)    {
    // dosya adlari sifrelenmisse cozme altprogrami
    HANDLE PASCAL hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;    // RAR dosyayla ilgili veriler
    struct RARHeaderData *Baslik;    // RAR icerisindeki dosyalarla ilgili veriler
    char sozluk[] = "sozluk.txt";    // sozluk.txt denenecek kelimelerin oldugu dosya
    char kelime[256];
    FILE *hDosya;
    int status;

    hDosya = fopen(sozluk, "r");

    AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));
    Baslik     = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));

    while(!feof(hDosya))    {
        // sozlukten bir kelime oku
        fscanf(hDosya, "%s", kelime);

        // RAR kutuphanesinin dosya acmak icin kullandigi yapiyi doldur:
        AcmaVerisi->ArcName    = arsiv;
        AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
        AcmaVerisi->CmtBuf     = NULL;
        AcmaVerisi->CmtBufSize = 0;

        // doldurulan verilerle RAR dosyayi ac
        hArsiv = RAROpenArchive(AcmaVerisi);

        Baslik->CmtBuf   = NULL;
        RARSetPassword(hArsiv, kelime);
        // sifreyi belirttikten sonra RAR dosya basligini oku
        status = RARReadHeader(hArsiv, Baslik);
   
        // eger baslik okuma isi basariyla sonlanmissa status'da sifir degeri
        // doner. yani sifre bulunmus olur. bu durumda donguyu sonlandir.
        if(status == 0)    break;   

        RARCloseArchive(hArsiv);
    }

   // sozluk.txt'nin sonuna gelinmemisse (break ile cikildiysa) sifre bulunmustur
    if(!feof(hDosya))
        // sifre bulunduysa ekrana yaz
        printf("Sifre: %s\n", kelime);
    else
        printf("Sifre bulunamadi.\n\n");

    fclose(hDosya);
    return;
}


int main(int argc, char *argv[])    {
    HANDLE PASCAL hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;
    struct RARHeaderData *Baslik;
    char dosyaadi[] = "deneme1.rar";   
    // programa parametre olarak herhangi bir dosya adi belirtilmezse bu dosya
    // adindaki RAR dosyayi acmaya calisir.
    int status, k1 = 0, k2 = 1;

    // RAR dosyanin acma verisi icin bellek ayir. malloc'un sonucunu kontrol
    // ettirmedim, ancak istenirse bu kontrol eklenebilir.
    AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));

    // programa bir arguman dosya adi verildiyse bu dosya adini ac.
    if(argc > 1)    strcpy(dosyaadi, argv[1]);

    AcmaVerisi->ArcName    = dosyaadi;
    AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
    AcmaVerisi->CmtBuf     = NULL;
    AcmaVerisi->CmtBufSize = 0;
   
    if((hArsiv = RAROpenArchive(AcmaVerisi)) == NULL)    {
        // dosyayi bulamazsa
        printf("Arsivi acma basarisiz oldu.\n");
        return 1;
    }

    // RAR dosyayi ac, baslik icin gereken yapilari bellekte ayir.
    Baslik = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));
    Baslik->CmtBuf   = NULL;    // RAR dosyadaki yorumlar okunmayacak

    while(1)    {
        // RAR dosya icerisindeki dosya bilgilerini oku
        status = RARReadHeader(hArsiv, Baslik);
        // eger RAR dosyanin sonuna gelindiyse while'i sonlandir.
        if(status == ERAR_END_ARCHIVE)    break;
        // dosya adlari sifrelenmisse ArsivCoz fonksiyonu cagirilacak
        if(status == ERAR_MISSING_PASSWORD)    {
            printf("Dosya adlari sifrelenmis.\n");
            ArsivCoz(dosyaadi);
            break;
        }

        k1 = k1 + k2;    // ilk adimda k1 = 0, k2 = 1. eger bir tane sifrelenmis
                        // dosya varsa k2 = 0 olacak, sayma islemi sonlandirilacak.
        printf("Arsivdeki dosyanin adi: %s", Baslik->ArcName);
        if(Baslik->Flags & 0x04)    {    // flags'da 3. bitin set edilmesi dosyanin
            printf("*\n");                // sifrelendigini gosterir. sifrelenmis
            k2 = 0;                        // dosyayi yaninda * ile ekrana yazip dosya
        }                                // sayimini sonlandir.
        else    printf("\n");            // dosya sifrelenmemisse * koyma

        status = RARProcessFile(hArsiv, RAR_SKIP, NULL, NULL);    // bir sonraki dosyaya gec
        if(status != 0)    printf("Dosyalari islerken birseyler oldu (%d).\n", status);
    }

    RARCloseArchive(hArsiv);

    // k1'de ilk sifreli dosyanin numarasi var.
    if(status != ERAR_MISSING_PASSWORD)
    // eger MISSING_PASSWORD varsa zaten tum dosya sifrelidir.
    // tek bir dosya sifreliyse DosyaCoz fonksiyonunu cagir.
        DosyaCoz(dosyaadi, k1);

    return 0;
}


Bu arada program kodunu vim'de yazıp Notepad++'ya aktardım. Vim'de tabstop 8, Notepad++'da 4 karakter. Bu nedenle de kodun bazı yerlerinde hizalama (indentation) kaydı. Beni suçlamak yerine alıcılarınızın ayarlarıyla oynayın lütfen.

Gelelim bilgisayarına MPI kuracaklar veya hesaplama kümesi erişimi olanlar için asıl olay paralel koda:


#include<stdlib.h>
#include<string.h>
#include<stdio.h>
#include<mpi.h>

// unrar kutuphanesine ait header dosyalari
#include<raros.hpp>
#include<dll.hpp>

#define MAX_PASS_LENGTH 16
#define MAX_NAME_LENGTH 256        // olabilecek en uzun dosya adi.
#define CALL_ArsivCoz   0x04
#define CALL_DosyaCoz   0x08
#define TAG             0x10

void DosyaCoz(char *arsiv, char *sozluk, int blksiz, int ofset, int sira)    {
    /*
        DosyaCoz(...): Dosya adlari acik, dosya sifreliyse cozme islemi
        arsiv:    RAR dosyanin adini tutan string.
        sozluk:   Sifrelerin listelendigi sozluk dosyasinin adini tutan string
        blksiz:   her bir islemciye sozluk dosyasindan dusen kelime sayisi
        ofset:    her islemcinin sozluk dosyasinin hangi satirini okuyacagi
        sira:     sifresi cozulecek dosyanin arsiv dosyasindaki sirasi
    */
    HANDLE PASCAL hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;    // RAR dosyayla ilgili veriler
    struct RARHeaderData *Baslik;  // RAR icerisindeki dosyalarla ilgili veriler
    char kelime[MAX_PASS_LENGTH];
    FILE *hDosya;
    int status, k1, k2, rank;
    double sure;

    MPI_Comm_rank(MPI_COMM_WORLD, &rank);    // MPI uzayinda kacinci islemciyim?
    sure = MPI_Wtime();        // sure olcumunu baslat

#ifdef DEBUG
        printf("(%d): blok buyuklugu: %d, ofset: %d\n", rank, blksiz, ofset);
#endif

    hDosya = fopen(sozluk, "r");
        for(k2 = 0; k2 < ofset; k2++)
            fgets(kelime, MAX_PASS_LENGTH, hDosya);
            // her islemci kendi okuyacagi yere kadar gelsin

    // RAR dosya ve icindeki verileri acmak icin gerekli yapilari bellekte ayir
    AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));
    Baslik     = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));
    // burada malloc'un dondurdugu degerlerin test edilmesi eksik
   
    k2 = 0;
    while(k2 < blksiz)        {
        // sozlukten bir kelime oku
        fscanf(hDosya, "%s", kelime);

#ifdef DEBUG
            printf("(%d): (sayac %d) denenen kelime: %s\n", rank, k2, kelime);
            fflush(stdout);
#endif

        // RAR kutuphanesinin dosya acmak icin kullandigi yapi
        AcmaVerisi->ArcName    = arsiv;
        AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
        AcmaVerisi->CmtBuf     = NULL;
        AcmaVerisi->CmtBufSize = 0;

        // RAR dosyasini ac
        hArsiv = RAROpenArchive(AcmaVerisi);
        Baslik->CmtBuf   = NULL;

        for(k1 = 0; k1 < (sira - 1); k1++)      {
        // ilk sifreli dosyaya kadar oku, yalnizca bazi dosyalar sifrelenmis
        // olabilir.
        RARReadHeader(hArsiv, Baslik);
        RARProcessFile(hArsiv, RAR_SKIP, NULL, NULL);
        }

        // ilk sifreli dosyayi oku
        RARReadHeader(hArsiv, Baslik);
        RARSetPassword(hArsiv, kelime);

        // sifreyle birlikte dosyayi test et. eger sifir dondururse sifre dogrudur.
        status = RARProcessFile(hArsiv, RAR_TEST, NULL, NULL);

        if(status == 0)    break;    // dogru sifre bulunduysa donguyu sonlandir

        RARCloseArchive(hArsiv);
        k2++;
    }

    if(k2 < blksiz)     {    // dongu bitiminde islemci kendisine dusen blogun
        // sonuna gelmemisse arada bir yerde sifreyi buldu demektir.
        printf("Sifre: %s\n", kelime);
        MPI_Abort(MPI_COMM_WORLD, 8);
    }
    else
        printf("Sifre bulunamadi.\n\n");

        printf("Islemci %d icin calisma suresi %f saniye.\n", rank, MPI_Wtime() - sure);
    fclose(hDosya);

    return;

}

void ArsivCoz(char *arsiv, char *sozluk, int blksiz, int ofset)    {
    /*
        ArsivCoz(...): Dosya adlari sifrelenmisse cozme algoritmasi
        arsiv:    RAR dosyanin adini tutan string.
        sozluk:   Sifrelerin listelendigi sozluk dosyasinin adini tutan string
        blksiz:   her bir islemciye sozluk dosyasindan dusen kelime sayisi
        ofset:    her islemcinin sozluk dosyasinin hangi satirini okuyacagi
    */

    HANDLE PASCAL hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;    // RAR dosyasiyla ilgili veriler
    struct RARHeaderData *Baslik; // RAR icerisindeki dosyalarla ilgili veriler
    char kelime[MAX_PASS_LENGTH];
    FILE *hDosya;
    int status, k1, rank;
    double sure;

    MPI_Comm_rank(MPI_COMM_WORLD, &rank);    // MPI uzayinda kacinci islemciyim?
    sure = MPI_Wtime();  // sure olcumunu baslat..

#ifdef DEBUG
    printf("(%d): blok buyuklugu: %d, ofset: %d\n", rank, blksiz, ofset);
#endif

    hDosya = fopen(sozluk, "r");

        for(k1 = 0; k1 < ofset; k1++)
            fgets(kelime, MAX_PASS_LENGTH, hDosya);
            // herkes sozluk dosyasinda kendi okuyacagi yere kadar gelsin

    // RAR dosya ve icindeki verileri acmak icin gerekli yapilari bellekte ayir
    AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));
    Baslik     = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));
    // burada malloc'un dondurdugu degerlerin test edilmesi eksik

    k1 = 0;
    while(k1 < blksiz)        {
        // sozlukten bir kelime oku
            fgets(kelime, MAX_PASS_LENGTH, hDosya);

#ifdef DEBUG
        printf("(%d): (sayac %d) denenen kelime: %s\n", rank, k1, kelime);
        fflush(stdout);
#endif

        // RAR kutuphanesinin dosya acmak icin kullandigi yapiyi doldur
        AcmaVerisi->ArcName    = arsiv;
        AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
        AcmaVerisi->CmtBuf     = NULL;
        AcmaVerisi->CmtBufSize = 0;

        // doldurulan verilerle RAR dosyayi ac
        hArsiv = RAROpenArchive(AcmaVerisi);

        // CommentBuffer = NULL : Comment'leri okuma
        Baslik->CmtBuf   = NULL;
        RARSetPassword(hArsiv, kelime);
        // sifreyi belirttikten sonra RAR dosya basligini oku
        status = RARReadHeader(hArsiv, Baslik);

        // eger baslik okuma isi basariyla sonlanmissa status'da sifir degeri
        // doner yani sifre bulunmus olur. bu durumda donguyu sonlandir.
        if(status == 0) break;

        RARCloseArchive(hArsiv);
        k1++;
    }

    if(k1 < blksiz)       {    // dongu bitiminde islemci kendisine dusen blogun
        // sonuna gelmemisse arada bir yerde sifreyi buldu demektir.
        printf("Sifre: %s\n", kelime);
        MPI_Abort(MPI_COMM_WORLD, 9);
    }
    else
        printf("Sifre bulunamadi.\n\n");

    printf("Islemci %d icin calisma suresi %f saniye.\n", rank, MPI_Wtime() - sure);
    fclose(hDosya);

        return;
}

int main(int argc, char *argv[])    {
    FILE *hSozluk;
    int size, rank, nsatir, krkt, mesaj, blksiz, ofset, status;
    int k1 = 0, k2 = 1;
    HANDLE PASCAL *hArsiv;
    struct RAROpenArchiveData *AcmaVerisi;
    struct RARHeaderData *Baslik;
    char dosyaadi[MAX_NAME_LENGTH];
    char sozluk[MAX_NAME_LENGTH] = "sozluk.txt";
    char tmp[MAX_PASS_LENGTH];
    double sure;

    MPI_Init(&argc, &argv);
    // MPI'i initle, islemci sayisini ve sira sayilarini al.
    MPI_Comm_size(MPI_COMM_WORLD, &size);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    sure = MPI_Wtime();

    if(argc > 1)    {
        // RAR dosyanin adi programa arguman olarak verilir.
        strcpy(dosyaadi, argv[1]);
        // eger birden fazla arguman verilmisse ikinci arguman sozluk dosyasidir
        if(argc > 2)    strcpy(sozluk, argv[2]);
    }
    else    {
        if(rank == 0)   printf("Kullanim: rarsolve <dosyaadi.rar> [sozluk.txt]\n");
        MPI_Abort(MPI_COMM_WORLD, 1);
        return 1;
    }

    if(rank == 0)    {
        // sifirinci islemci sozlugun satir sayisini belirler
        hSozluk = fopen(sozluk, "r");

        nsatir = 0;

        do    {
        krkt = fgetc(hSozluk);
        if(krkt == '\n')    nsatir++;
        } while(krkt != EOF);

        fclose(hSozluk);
    }

    // satir sayisini butun islemcilere bildir.
    MPI_Bcast(&nsatir, 1, MPI_INT, 0, MPI_COMM_WORLD);
   
    blksiz = nsatir / size;
    ofset  = rank * blksiz;
    // herkes kendine dusen sozluk blogunu ve ilk kelimesini bulsun

    if((nsatir % size) && (rank == (size - 1)))
        blksiz = nsatir - (blksiz * size - blksiz);
        // tamsayi duzenlemeleri

    if(rank == 0)    {
        // RAR dosyanin acma verisi icin bellekte yer ayir.
        AcmaVerisi = (struct RAROpenArchiveData *)malloc(sizeof(struct RAROpenArchiveData));

        AcmaVerisi->ArcName    = dosyaadi;
        AcmaVerisi->OpenMode   = RAR_OM_EXTRACT;
        AcmaVerisi->CmtBuf     = NULL;
        AcmaVerisi->CmtBufSize = 0;

        if((hArsiv = RAROpenArchive(AcmaVerisi)) == NULL)   {
            // dosyayi bulamazsa
            printf("Arsivi acma basarisiz oldu.\n");
            MPI_Abort(MPI_COMM_WORLD, 2);
            return 2;
        }

        // RAR dosyayi ac, baslik icin gereken yapilari bellekte ayir.
        Baslik = (struct RARHeaderData *)malloc(sizeof(struct RARHeaderData));
        Baslik->CmtBuf   = NULL;    // RAR dosyadaki yorumlar okunmayacak

        while(1)    {
            // RAR arsivi icerisindeki dosya bilgilerini oku.
            status = RARReadHeader(hArsiv, Baslik);
            // eger RAR dosyanin sonuna gelindiyse while'i sonlandir.
            if(status == ERAR_END_ARCHIVE)  break;
            // dosya adlari sifrelenmisse ArsivCoz fonksiyonu cagirilacak.
            if(status == ERAR_MISSING_PASSWORD)     {
                printf("Dosya adlari sifrelenmis.\n");
                mesaj = CALL_ArsivCoz;
                for(krkt = 1; krkt < size; krkt++)
                    // sifirinci butun islemcilere ArsivCoz fonksiyonunun
                    // cagirilacagini mesajla bildiriyor.
                    MPI_Send(&mesaj, 1, MPI_INT, krkt, TAG, MPI_COMM_WORLD);
                // sifirinci islemcinin kendisi de ArsivCoz'u cagiriyor.
                ArsivCoz(dosyaadi, sozluk, blksiz, ofset);
                break;
            }

            k1 = k1 + k2;   // ilk adimda k1 = 0, k2 = 1. eger bir tane sifrelenmis dosya varsa
                            // k2 = 0 olacak, sayma islemi sonlandirilacak.
            printf("Arsiv adi: %s", Baslik->ArcName);
            if(Baslik->Flags & 0x04)  {     // flags'da 3. bitin set edilmesi
                printf("*\n");          // dosyanin sifrelendigini gosterir.
                k2 = 0;                  // sifrelenmis dosyayi yaninda * ile yaz
            }
            else    printf("\n");

            status = RARProcessFile(hArsiv, RAR_SKIP, NULL, NULL);  // bir sonraki dosyaya gec
            if(status != 0) printf("Dosyalari islerken birseyler oldu (%d).\n", status);
        }

        RARCloseArchive(hArsiv);

        // k1'de ilk sifreli dosyanin numarasi var.
        if(status != ERAR_MISSING_PASSWORD)    {
        // eger MISSING_PASSWORD varsa zaten tum dosya sifrelidir.
                mesaj = CALL_DosyaCoz;
            // sifirinci islemci diger islemcilere DosyaCoz fonksiyonunun
            // cagirilacagini mesajla bildiriyor
                for(krkt = 1; krkt < size; krkt++)
                    MPI_Send(&mesaj, 1, MPI_INT, krkt, TAG, MPI_COMM_WORLD);
                // sifirinci islemcinin kendisi de ArsivCoz'u cagiriyor.
                DosyaCoz(dosyaadi, sozluk, blksiz, ofset, k1);
            }
    }

    else    {
        // sifirinci islemci haricindeki islemciler sifirincidan mesaj bekliyor
        MPI_Recv(&mesaj, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
        if(mesaj == CALL_ArsivCoz)   
            ArsivCoz(dosyaadi, sozluk, blksiz, ofset);
        else if(mesaj == CALL_DosyaCoz)
            DosyaCoz(dosyaadi, sozluk, blksiz, ofset, k1);
    }

    if(rank == 0)
        printf("Toplam %f saniyede %d kelime denendi.\n", MPI_Wtime() - sure, nsatir);
   
    MPI_Finalize();

    return 0;
}



Son olarak herşey tamamsa son bir soru kalıyor: "Sözlük dosyaları nasıl oluşturulacak?" Onun için de basit bir MATLAB kodu yazdım. Yaptığı tek şey belirli kriterlere göre 5 ile 10 karakter arası rastgele kelimeler üretip bunları sozluk.txt dosyasina yazıyor. Aslında bunun için daha amaca daha yönelik araçlar yazılabilir. Örneğin sozluk.txt olarak imla kılavuzunu alıp imla kılavuzundaki kelimelerin başına yada sonuna sayılar eklemek gibi. Matlab kodu görece basit olduğundan pek açıklama yazmayacağım ancak onunla ilgili tek söyleyebileceğim 33 ve 36. satırlardaki iki for döngüsü yüzünden bir hayli yavaş çalıştığı. 


kucuk_harfler = true;
buyuk_harfler = false;
sayilar       = true;
karakterler   = false;
kel_sayi = 200000;        % 200bin kelime uret

kHavuz = [ ];
bharf_tablo = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
kharf_tablo = 'abcdefghijklmnopqrstuvwxyz';
sayi_tablo  = '0123456789';
karak_tablo = '!#$%&()*+,-./:;<=>?@_';

if(kucuk_harfler)
    kHavuz = strcat(kHavuz, kharf_tablo);
end

if(buyuk_harfler)
    kHavuz = strcat(kHavuz, bharf_tablo);
end

if(sayilar)
    kHavuz = strcat(kHavuz, sayi_tablo);
end

if(karakterler)
    kHavuz = strcat(kHavuz, karak_tablo);
end

lHavuz = length(kHavuz);
hSozluk = fopen('sozluk.txt', 'w');

for k1 = 1:kel_sayi
    kelime = [ ];
    uzunluk = floor(rand * 5 + 5);    % kelime uzunluklari rastgele
    for k2 = 1:uzunluk
        Havuz_ptr = floor(rand * lHavuz + 1);
        kelime = strcat(kelime, kHavuz(Havuz_ptr));
    end
    fprintf(hSozluk, '%s\n', kelime);
   
end

fclose(hSozluk);



Buraya kadar kodlardan ötürü bir hayli uzunca bir yazı oldu. Bu ara tekrar yazmak için zaman bulabilirsem gerçekten ne kadar gereksiz işlerle uğraştığımı anlatan bir yazı daha yayınlayabilirim.