13 Kasım 2013 Çarşamba

Office Dosyalarından Şifre Kaldırmak


Varsayalım ki elimde değiştirilmeye karşı korunmuş .doc dosya biçiminde bir form var. Ben örnek olarak bir konferansın katılım formunu ele alacağım. Muhtemelen organizatörler formları daha sonra OCR işleyeceklerinden satır sayıları ve gerekli bilgi alanları kısıtlanmış. Yerlerini yada satır sayılarını bile değiştirmek olanaksız. Diyelim ki benim konferansa yollamak istediğim bildirimin adı organizatörlerin öngördükleri kadar kısa değil ve ben kendi seçtiğim başlığı kısaltmamak için inanılmaz bir inat içerisindeyim. İngilizce Word kullandığımdan ötürü menüleri İngilizce yazmak zorundayım. Review menüsünden Protect Document'a oradan da 'Restict Formatting and Editing'e basıyorum. Sağ tarafta en altta 'Stop Protection' diye bir düğme çıkıyor, ona basınca şifreyi soruyor.


Önce çok uğraşmamak için piyasada bulunan ve .doc dosyalardan şifre kurtardığını iddia eden programları denedim. Hepsi paralı ve açıkçası şifreyi doğru olarak bulduklarını düşünmüyorum. Denediklerimden bir tanesi şifrenin iki karakterini açık olarak, geri kalanları '*' karakterleriyle gösterdi. Başka birisi karakter sayısını öncekinden farklı bildirdi. İçlerinde kötü niyetli yazılımlar olacağından bile kuşkulanıyorum. Bunlardan vazgeçtim, kolları sıvadım.

Elimdeki dosya .doc biçimindeydi. En başta içeriğine baktım. Şifre, dosya içerisinde şifrelenmemiş biçimde tutuluyor olabilir diye düşündüm ve karakter dizilerini inceledim. Daha önce denediğim şifre çözücülerin verdiği eksik çıktıya benzer bir deseni dosya içerisinde arattım ama bir sonuca varamadım. Sonra basit bir rotate yada xor ile şifrelendiğini düşünerek XOR Search'ü denedim. Sonuçta yine herhangi bir sonuç elde edemedim. Sonunda bundan da vazgeçtim.

Dosya içeriğine değil şablonuna koruma konulduğu için dosyayı açabiliyorum. Dosyayı .docx biçiminde farklı kaydettim. Bu dosya biçimini inceleyenler bilecektir. Önceki dosya biçimlerinden farklı olarak 2007'den itibaren MS Office dosyaları XML biçiminde kaydedilip zip olarak sıkıştırılmaya başlandı. Hexeditor ile içeriğine bakınca PK imzasından tanınabiliyor.


.docx uzantısını .zip yapıp dosyayı bir dizine açtım. grep ile biraz aratınca şifrenin RSA ile şifrelenmiş bir biçimde word/settings.xml dosyasında tutulduğunu buldum.

Seni gidi seni...


Bundan sonra yapılacak iş, görüntüde de görülen document protection tag'ını tümden silmek, XML dosyasını kaydedip çıkmak. Sonra zip dosyasının eski yapısını bozmadan tekrar sıkıştırmak. Sıkıştırırken mümkün olduğu kadar varsayılan ayarları kullanmaya çalıştım. WinRAR kullanarak zip sıkıştırmasını seçtim, sıkıştırma yöntemi olarak 'Normal' ve sözlük büyüklüğü olarak '32 KB' seçtim (zaten başka bir sözlük büyüklüğü seçmeme izin vermiyor). .zip dosyasını tekrar oluşturdum ve .docx olarak tekrar uzantısını değiştirdim. Dosya sorunsuz açıldı. Bundan sonra 'Paper Title' yazan satırı biraz aşağı çekerek büyüttüm. İstediğimi yazacak kadar genişletip derin bir nefes aldım. Açıkçası o kadar yazılımcı veya mühendis çalıştıran bir Microsoft'tan bundan daha iyisini beklerdim.

6 Kasım 2013 Çarşamba

Türk'ün LittleAlchemy'yle İmtihanı


Bundan hemen önce yazdığım Unrar kütüphanesi serisinde, işlerden zaman buldukça yazmaya devam edeceğimi yazmıştım ancak durumlar hiç de benim beklediğim gibi olmadı. Aslında iş yoğunluğu normale döndü ancak çalışma saatleri dışında oyun oynamaktan ötürü yazmaya zaman ayıramadım. Ben de madem oyun oynuyorum, oyunla ilgili birşeyler yazmaya karar verdim.

Üzerinde konuşacağım oyun Little Alchemy. Aslında 2010 yada 2011'de çıkmış eski bir oyun ama ben keşfedeli çok zaman olmadı. Bilmeyenler olabileceğini düşünerek özetlenirse; oyun 4 temel elementle başlıyor: Hava, Su, Ateş, Toprak. Amaç, bu temel elementleri birleştirerek diğer maddeleri oluşturmak. Bu yazıyı yazmamdan 3 gün öncesine kadar veritabanında 420 element vardı, şu anda 430 tane var. Birkaç ayda bir kullanıcılardan gelen geri beslemeye göre element listesi güncelleniyor. Little Alchemy çok basit bir oyun ancak RSA şifrelemesi gibi tamamlamak için çok fazla olasılık var ki oyunu asıl zor hale getiren de bu. Bütün farklı kombinasyonları tek tek denemeye çalışmak pratikte imkansız gibi.

Element birleştirme işlemi sağ taraftaki elementler listesinden elementleri sol tarafa sürükleyip birbirlerinin üstüste çakıştırmak biçiminde. Örneğin Ateş + Toprak = Lav, Lav + Hava = Taş, Hava + Toprak = Toz, Toz + Ateş = Barut ... biçiminde ilerliyor. Element sayısı az olduğu zaman birleştirmek kolay ama element sayısı arttıkça elementlerin arasında gezinirken genelde neyi neyle birleştirdiğinizi unutup bir yerden sonra olduğunuz yerde saymaya başlıyorsunuz. Dolayısıyla elle hiçbir yardım olmadan denemek bir yerden sonra etkili olmuyor. Bunu fark ettikten sonra yaptığım işlemleri Excel tablosuna aktarmaya başladım. Bir yerden sonra sonuçlar tabloya da sığmaz hale geldi, üstelik hem daha önce denediğim bazı kombinasyonları tekrar denememi önlemiyordu hem de tabloda sürekli arama yapmak pratik değildi.

Oturup çözüm için basit bir algoritma geliştirdim ve MATLAB'de kodladım. Mantığı basit ve büyük olasılıkla da en optimal algoritma değil. Sadece beni aynı şeyleri tekrar denemekten kurtarsın diye yazdım. Yazıyı uzatmamak için sadece pseudokodu ekleyeceğim. Kodun içerisinde dört sütundan oluşan bir tablo tutuyorum. En başta satır sayısı elimdeki element sayısına eşit yani dört. İlk sütun elementin adı. İkinci ve üçüncü sütunlar elementin bileşenleri (ilk 4 element için sıfır.) Son sütun da elementin başka elementler verip vermeyeceğine dair bir flag. Dikkat edilirse bazı elementlerin üstüne gelince arkaplan hafif kırmızı oluyor. Bu elementler başka elementlerle daha fazla etkileşime girmiyorlar, ağacın yaprakları gibi en uçta bulunuyorlar. Örneğin 'Obsidian' gibi (water + lava). Dolayısıyla yeni element ararken zamandan kazanmak için bunları tekrar etkileşime sokmaya gerek yok. Elementlerin kodları da listeye eklendiği sıra sayıları oluyor. Bir de etkileşim tablosu tutuyorum. Bu, o anda bildiğim element sayısı kadar dereceye sahip bir kare matris. Yani ilk adımda 4x4 bir matris. Matrisin i. satır ve j. sütun elemanının değerine göre, i. elementle j. element etkileşime girmiş mi giremiş mi bunu tutuyorum. Henüz denememişsem değeri -1, etkileşim olmuyorsa 0, etkileşim oluyorsa 1. İlk değeri -1'lerden oluşan 4x4'lük bir matris. Elbette ki değişme özelliği bulunduğundan i. ile j.'yi karıştırmakla j. ile i.'yi karıştırmak arasında herhangi bir fark yok. Yani aslında  sadece matrisin üst yada alt üçgen kısmını tutmak yeter ancak kafa karıştırıcı olmaması açısından ben her ikisini de tutacağım. Bu matrisin bütün çalışma süresince simetrik olacağı açık.

Yukarıda bahsettiğim iki tablo kodun kritik değişkenleri. Öyle ki bu değerleri belli aralıklarla diske yazıyorum, çalışmaya ara vermem gerektiğinde sadece bu iki tabloyu yükleyerek çalışmaya kaldığım yerden devam edebiliyorum. İki tablonun içeriği oluşturulduktan sonra sürekli olarak kullanıcıya şununla (i. element olsun) şunu (j. element olsun) karıştır sonucu yaz diyor. Eğer bir sonuç varsa bunu listeye ekleyip etkileşim matrisinin (i, j) ve (j, i) elemanlarını 1 yapıyor. Bütün karşılıklı etkileşimleri denedikten sonra yeni eklenenler kadar etkileşim matrisini genişletiyor ve yeni baştan yeni eklenen elementlerle denemeler yapıyor.

Pseudokod şöyle:
ilk 4 elementle veritabanini ve etkilesim matrisini olustur.

while(toplam < 430)
  eleman_sayisi = veritabani.size;
  toplam = eleman_sayisi;
  for k1 = 1:eleman_sayisi
  . k1. eleman tepkime veriyorsa;
  . for k2 = 1:eleman_sayisi
  . . k2. eleman tepkime veriyor ve k1 ile k2'nin etkilesimi bilinmiyorsa;
  . . sonuc = input( element(k1) + element(k2) = ? );
  . . eger sonuc yoksa etkilesim(k1, k2) = etkilesim(k2, k1) = 0;
  . . sonuc varsa
  . .   ayni element daha once listeye girilmemisse 
  . .     adini gir, onculler k1 ve k2 gir
  . .     baska etkilesime giriyorsa 4. sutun true, degilse false
  . .     etkilesim(k1, k2) = etkilesim(k2, k1) = 1;
  . .     toplam = toplam + 1;
  . .   daha once girilmisse
  . .     etkilesim(k1, k2) = etkilesim(k2, k1) = 0;
  . endfor
  endfor

  eklenen = toplam - eleman_sayisi;
  etkilesim matrisinin sagina (eleman_sayisi x eklenen) boyutlu -1'lerden olusan matrisi ekle
  etkilesim matrisinin altina (eklenen x (elesay + eklenen) boyutlu -1'lerden olusan matrisi ekle

end


Programı çalıştırınca while döngüsünün ilk adımında sırayla şu yeni elementler oluşacak:
  1. Su + Su = Deniz
  2. Su + Ateş = Buhar
  3. Su + Toprak = Çamur
  4. Su + Hava = Yağmur
  5. Ateş + Toprak = Lav
  6. Ateş + Hava = Enerji
  7. Toprak + Toprak = Basınç
  8. Toprak + Hava = Toz
  9. Hava + Hava = Basınç
9. adımdaki basınç 7. adımda bulunduğundan listeye toplam 8 element eklenecek. Bazı elementler birden fazla biçimde yapılabildiği için kodda elementin daha önceden listeye eklenip eklenmediğine dair bir kontrol gerekiyor. Ayrıca bazı elementlerin birleşimi sonucunda birden fazla yeni element oluşabiliyor. Bu durumda da tablolara elle müdahale etmek gerekiyor.

Yukarıdaki kodun ne kadar çalışacağını kestirmek için bir elementin derecesi diye bir kavram ortaya attım. while'in ilk adımında ortaya çıkan (yani yukarıda saydığım) elementlere birinci derece elementler diyelim. Bir sonraki adımda bunlarla etkileşime girince oluşacaklar ikinci derece elementler olsun. En başta elimizde olan dört element sıfırıncı derece elementler olsun. Sıfırıncı dereceden birinci dereceye geçişte element sayısı 3 katına artıyor. Bu durumda ikinci derecede 36 element, üçüncü derecede 108, dördüncü derecede 324 element ve 430 element bulunduğundan bütün elementler en fazla beşinci derecede bitecektir diye kaba bir hesap yaptım. Başka tahmin modelleri de uygulanabilir ancak bulduğum diğer modeller de elementlerin yaklaşık beşinci derecede biteceğini öngörüyor. Elle beşinci dereceye kadar gelebilmek 1-1.5 saatimi aldı fakat evdeki hesap çarşıya uymadı. Beşinci derece elementlerle birlikte sadece 110 elemente ulaşabildim. Bu algoritmaya uymadan rastgele denemelerle, 180'e yakın element ürettiysem de bir sistematiğe oturtmadan bütün elementleri bulmak bence imkansız. Yaklaşık 2-2.5 saat sonra bütün altıncı derece elementleri de tamamladım fakat 164 tane oldu. 200'ü bile bulamadım.

Yedinci derece elementleri taramaya başladıktan sonra bu işin elle de yapılamayacağını fark ettim. Öyle ki bütün elementleri bulmak için işten 3-5 gün izin almayı bile düşündüm. Sonra bir ihtimal diyerek sayfanın kaynağını incelemeye başladım. Bütün java scriptleri indirip inceledim (ki hiç java script yazmamış bir insan olarak anlayabildiğim, Macarca bir diyalogda sadece arada geçen Michael Jackson kelimesini anlayıp sevinmek eylemine benzetilebilir). Kodların içinde, elementlerin ad ve birleşim tablolarını tutan iki listenin adına rastladım. Bunları oyunu yapanların haklarına saygısızlık olacağı gerekçesiyle burada ilan etmeyeceğim ancak ilgilenen ve yeteneğine güvenenler varsa kodları kendi inceleyebilir. Yalnız bir ipucu vereyim, tablolar JSON biçiminde saklanıyor. Bunları indirip, Notepad'in karakter değiştirme işleviyle bile halledilebilecek ufak birkaç değişiklikle MATLAB'in anlayacağı biçime dönüştürdüm ve tek bir for döngüsünden oluşan bir sorguyla bütün çözümleri buldum. Veritabanıyla ilgili bir not daha: Oyunun sanıyorum paralı sürümü var ve bedava sürümünden 30 element fazlası var. Veritabanında bu elementler görünüyor ancak bileşenleri anlamsız girdilerden oluşuyor. Veritabanını indirenler ne dediğimi anlayacaklardır.
Ömrümü yedin.

Gelelim tahminin hatasına. İlk 4 element birbirleriyle yoğun olarak tepkimeye giriyorlar, sonraki derece elementler bu kadar yoğun olarak etkileşmiyorlar. Dolayısıyla ilk adımlarda yapılan tahminler her şekilde iyimser kalıyor. Her derecedeki element sayısını görselleştirince aşağıdaki grafik çıkıyor:


Aslında artış üstel olarak devam etmesi gerekirken element sayısının sınırlı olması nedeniyle 9. dereceden sonra artış çok azalıyor. Bir de elbette ki benim tüm tahminlerim etkileşimin sınırsız olması üzerine ancak derece ilerledikçe elementler karmaşıklaşıyor ve birbiriyle etkileşimi zorlaşıyor. Basit elementlerin birbiriyle etkileşimi daha kolay. Vampirle çalar saatin veya aslanla şarabın etkileşime girmesi sanırım imkansız.

Bütün elementleri bulduktan sonra kafam biraz rahatladı ve işten izin almama gerek kalmadı ama tam olarak değil. Bu sefer benim yaklaşımımı incelemek için yukarıda yalancı kodunu verdiğim programı çok az değiştirip oyunun veritabanını kullanarak bir denemeye tuttum. Yaptığım çok basit; elle girdiğim girdileri veritabanına bakarak olup olmayacağını deniyor. Kodu yine çabucak MATLAB'de yazdım. Veritabanı zaten MATLAB matris biçimindeydi. Bazı elementleri elde etmenin birden fazla yolu olduğundan sorgulama uzun sürdü ancak sorgulamanın asıl uzun sürmesinin nedeni MATLAB'de yazdığım halde find'la halledilmesi gereken yerlerde fazlaca for döngüsü kullanmış olmam. Zaten algoritma da en optimal halinde değil. Bütün bu olumsuzluklar birleşince elementlerin tamamını sayıp dökmesi 2-3 saatini alıyor. Benim yaklaşımımla çözünce, en fazla 16. dereceden iki tane element var ( meteor = atmosphere + meteorid , solar system = sun + planet ). Toplam karşılaştırma sayısı 92665 ( = 430 * 431 / 2 ). Bir günün 86400 sn olduğunu gözönüne alınca dikkate değer bir sayı, üstelik her saniyede bir karşılaştırma yapmak olanaksız. Liste çok uzadığında gerekli elementi bulmak bile zaman kaybettiriyor. Uzun lafın kısası, bütün elementleri tek tek denemek yazının başında RSA şifrelemesiyle ilgili yaptığım benzetmeyi haklı çıkardı.

Elementler üzerinde kafa yorulacak başka bir problem de bütün elementlerin temel dört element cinsinden yazılmasıyla ilgili. En başta bunun elementin derecesiyle birebir aynı olacağını düşünüyordum ama değil. Şöyle bir örnek vereyim eruption ( = energy + volcano ) üçüncü derece element ama temel elementler cinsinden yazıldığı zaman eruption = ( earth +( earth + fire ))+( air + fire ) halinde 5 temel element ile yazılabiliyor. Sonradan düşünce bunun nedenini anlamak zor değil. Belki biraz cebir bilgisi gerekirebilir.

Bütün elementlerim en fazla üçüncü dereceden elementler olsalardı dediğim doğru olurdu. Şöyle açıklanabilir; ilk başta elimde sadece 1 sayısı ve kural olarak toplama işlemi var. Bununla sadece 2 elde edebilirim. İkinci adımda elimde {1, 2} kümesi oldu. Bunlarla 1+2 = 2+1 = 3 elde edebilirim. Üçüncü adımda {1,2,3} kümesinden elde edebileceklerim artık kümenin en büyük elemanından bir fazlası değil çünkü 2+3=5. Başka bir yaklaşıma göre de, elimdeki n. derece elementlerden n+1. derece elementleri elde ederken yalnızca dört temel elementi eklemeye iznim olsaydı (yani bir önceki örnek üzerinden gidersek, kümenin bir sonraki elemanını elde ederken, keyfi bir eleman değil yalnızca 1'i alma hakkım var diyelim) o zaman yine doğru olacaktı.

Eruption'da da benzer şey geçerli, birinci derece bir elementle ikinci derece bir element etkileşime giriyor. Tabi derecelendirme sıfırdan başladığından yaptığımız 2+3 işlemi oluyor.

Tüm elementleri temel elementler türünden yazma sorununu ele alırken yine MATLAB kullandım. Önce element veritabanını basitleştirdim. Bazı (yaklaşık 200 tane) elementlerin birden fazla biçimde oluşturulabildiğini söylemiştim. Bunları gözönüne almadan, veritabanında karşıma ilk çıkanı aldım ve üç sütunluk bir tablo oluşturdum. Elementin adı, bileşen1, bileşen2. İlk dört element için bileşen1 ve bileşen2 değeri 0. Sonra basit özyinelemeli (recursive) bir fonksiyon yazdım.

function cozumle(element)
// negatif sayilar ozel karakterler: -9='+', -6='(', -3=')'
bil1 = veritabani(element).bilesen1;
bil2 = veritabani(element).bilesen2;

if( (bil1 <= 4) && (bil2 <= 4) )
  return [ -6, bil1, -9, bil2, -3 ];
elseif( (bil1 <= 4) && (bil2 > 4) )
  return [ -6, bil1, -9, cozumle(bil2), -3 ];
elseif( (bil1 > 4) && (bil2 <= 4) )
  return [ -6, cozumle(bil1), -9, bil2, -3 ];
else
  return [ -6, cozumle(bil1), -9, cozumle(bil2), -3 ];
end

Bundan sonrası gelen sayıları temel element adlarına yada (, ), + karakterlerine çevirip ekrana yazdırmak. Buna göre en fazla sayıda temel elementle yazılabilen 'ghost' var ( = night + graveyard ), tam 163 tane. Bunu takip eden 158 ile 'constellation' ve 'galaxy' var. Bazı elementlerin birleşimi sonucu birden fazla element çıktığını söylemiştim. Bu iki element de aynı şekilde 'star + star' sonucu ortaya çıkıyor. Bunlardan sonra üçüncülük 139 elementle 'pigeon'da ( = bird + city ). Bir de son olarak bunların dağılımına bakarsak asıl kümelenme 2 ile 20 arasında fakat 60'a kadar ciddi bir miktar var. 


Son olarak, bütün bu yazdıklarıma ek birşey daha kaldı. O da oyundaki elementlerin çizgesini (graph) daha doğrusu ağacını çıkartmak ancak buna uygun bir program bulamadığım için onu şimdilik yapmayacağım. 

Sonuç olarak sanıyorum ki bu yazıda dünyanın en basit oyunu üzerine dünyanın en uzun incelemesini yaptım. Yeri geldi sevdiklerime ayırmam gereken zamanda deli gibi MATLAB kodu yazdım, yeri geldi işe 4 saat uykuyla gittim. Sonunda da elime oyunu bitirmiş olmaktan başka birşey geçmedi. Böyle de duygusal bir kapanış oldu.

23 Ağustos 2013 Cuma

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


Unrar kütüphanesi beş tane fonksiyonuyla (aslında yedi ancak ben beşini anlattım) programcıya kolaylıkla .rar dosyaları için arabirim sağlıyor. Kütüphanenin bazı kısıtları var, bunun en başında hız geliyor. Hız kısıtlaması .rar dosyaların yapısından kaynaklı olabilir. Programı yazarken karşıma çıkan sıkıntı her şifre denemesi için dosyanın kapatılıp yeniden açılması oldu. Bu her defasında arşiv dosyası için gerekli bellek yapılarının silinip bellekte yeniden ayrılması demek. Belki gelecekte kodu daha da hızlandırmak için kendi .rar kütüphanemi yazmam gerekebilir. Yazının bu kısmında paralel programlama ve MPI paralel programlama kütüphanesini anlatacağım.

Büyük işlem gücü gerektiren işlemlerin küçük parçalara ayrılarak paralel olarak işlenmesi fikri yeni değil. Paralel hesaplamayı popüler hale getiren SETI@Home projesi bundan 15 yıl önce başladı. Daha sonra United Devices'ın kanser ve şarbona karşı ilaç için molekül araştırma projeleri geldi. Diğer yandan 2000'lerin başında 3DS MAX render işlerini küçük parçalara bölüp şebeke üzerinden birbirine bağlı makinalara gönderebiliyordu. 3DS MAX'i çalıştıran ve render işini başlatan bir ana makina vardı. Diğer makinalarda hesaplama işleri için istemciler kuruluydu. Ana makina iş parçalarını diğer makinalara dağıtıyor, sonuçları alıyor, işleyip renderi tamamlıyordu. Günümüzdeyse küme bilgisayarlar (Cluster Computing) artık yüksek performanslı hesaplama işlerinin vazgeçilmez bir parçası. Birçok araştırma merkezinde bu bilgisayarlardan kurulu ve bilgisayarların konfigürasyonları basit ve eski masaüstü Pentium 4'lerden 16 MB cep belleğe (cache) sahip son teknoloji Xeon'lara kadar geniş bir yelpazede yer alıyor. Üstelik düşük bütçeli araştırma merkezleri bile ikinci el bilgisayarlarla Beowulf kümeleri kurabiliyorlar. Son olarak bu konuda nerd'lüğün en uç noktası Flash Mob Computing geliyor. Büyükçe bir merkezde bir araya gelen ve bilgisayarlarını getiren katılımcılar dağıtılan bir linux CD'siyle bilgisayarlarını açıyorlar. Dağıtımda, gerekli istemci programlar kurulu geliyor. Katılım sağlandıktan sonra bütün bilgisayarların işlem gücü ana makinada toplanıyor. Gerekli hesaplama yapılıp bittikten yada akşam olduktan sonra herkes bilgisayarını alıp tekrar evine dönüyor.

MPI (Message Passing Interface) bilgisayar kümelerinde artık standart hale gelmiş bir arayüz. MPI, dağıtık bilgisayarlarda basit bir programlama arayüzüyle TCP/IP yada sunucu/istemci kodlarıyla uğraşmadan belli sayıda bilgisayarda çalışarak işleri paralel olarak çalıştırmaya olanak veriyor. Kütüphanenin mantığı oldukça basit: Bütün işler aynı anda programı çalıştıran kişinin belirttiği sayıda spawn ediliyor. Aslında bütün makinalarda çalışan kod birbirinin aynı. MPI dilinde bu makinalara artık "işlemci" (processor) deniyor. Her işlemcinin kendine ait bir numarası bulunuyor. Makinaların birbirinden farklı davranması bu numaraya göre sağlanabiliyor. Aynı zamanda toplam kullanılabilen makina sayısı da alınabiliyor.

Örneğin MPI ile 8 işlemci kullanılarak [a, b] aralığında integral alınacak olsun. Bu durumda işlemci sayısı n olmak üzere her bir işlemciye t = (b - a) / n kadarlık bir aralık düşecektir. Her işlemcinin numarası p olursa, işlemciler (a + p * t)'den (a + (p + 1) * t)'ye kadar olan bölümü integral alacaklar demektir. İntegral alma işleminde parçalar birbirine bağlı olmadığından işlemciler arası herhangi bir haberleşme olmadan birbirlerinden bağımsız hesaplamayı tamamlayabilirler. Hesaplama bittiğinde her işlemcinin bulduğu sonuç bir bilgisayara gönderilir ve orada toplanır. İntegralin sonucu, bulunan bu toplamdır. Bu örnekte .rar şifresini bulurken de herhangi bir veri bağımlılığı bulunmadığından veri bağımlılığı bulunan karmaşık örneklerden kaçınmaya çalıştım.

Arşivin şifresini bulmak için sözlük tabanlı deneme yapan başka programlar da piyasada kolayca bulunabilir. (Mesela RAR Password Cracker) Bunun yanında MPI tabanlı MD5 reverse hash algoritması yada Paralel John the Ripper bulabilmek de mümkün. Ancak MPI tabanlı RAR çözücü ben rastlamadım, bu nedenle yazdığım bu programın piyasada ilk olduğunu iddia edebilirim.

Elbette ki bütün MPI kütüphanesini burada anlatmam mantıklı olmayacağından ben sadece programda kullandığım fonksiyonları anlatacağım.

int MPI_Init( int *argc, char ***argv ): Bu fonksiyonla bütün işlemciler paralel işlem yapmaya hazırlanır, başka bir deyişle işlemcilere hazır olmalarını, diğer MPI fonksiyonlarının çağırılacağını bildirir. Fonksiyonun argümanları main(int argc, char *argv[]) fonksiyonunun argümanlarıyla aynıdır (gösterici olmaları dışında). Hemen her MPI programı ilk olarak bu fonksiyonu çağırmak zorundadır.


int MPI_Comm_size( MPI_Comm comm, int *size ): MPI_Init çağırıldığında bir haberleşme dünyası oluşturulur. Haberleşme dünyası tüm işlemcileri kapsayan bir üst küme olarak düşünülebilir.
http://nf.nci.org.au/training/MPIProg/slides/slides.028.html

MPI_Init çağırılır çağırılmaz oluşturulan default haberleşme dünyasının handleMPI_Comm türünde bir sabit olan MPI_COMM_WORLD'dür ve bu haberleşme dünyası bütün işlemcileri içerir. Daha sonra istenirse bunun alt kümesi olan yeni haberleşme dünyaları da oluşturulabilir. MPI_Comm_size fonksiyonu verilen comm haberleşme dünyasının içerdiği işlemci sayısını size adı verilen değişkene yazar. Yandaki resimde fonksiyon size değişkeninde 7 değerini döndürecektir. Bu fonksiyonun aynı haberleşme dünyası içerisindeki bütün işlemciler için aynı değeri döndüreceği açıktır.


int MPI_Comm_rank( MPI_Comm comm, int *rank ): Bu fonksiyon comm ile belirtilen haberleşme dünyasında her işlemciye özgü olan sıra numarasını rank değişkeninde döndürür. Bu fonksiyonun aynı haberleşme dünyası içerisinde bütün işlemciler için farklı değeri döndüreceği açıktır.


int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm): MPI_Send fonksiyonu işlemcilerin kendi arasında haberleşmeyi sağlayabilmeleri için gerekli en basit fonksiyondur. buf ile belirtilen bellek bölgesinden başlayarak, count tane MPI_Datatype türündeki datatype'ı, dest ile belirtilen işlemciye tag etiketiyle comm haberleşme dünyası içerisinde yollar.

İfade biraz açılacak olursa; buf önceden bellekte ayrılmış herhangi bir değer yada dizi olabilir. count bu dizinin büyüklüğüdür. MPI'da C'deki veri tiplerine karşılık gelen veri tipleri MPI_Datatype türünde kodlanmıştır. datatype değişkeni MPI_INT, MPI_DOUBLE, MPI_CHAR vb. yada bunlardan türetilmiş bir veri türü olabilir. Bu türün büyüklüğü count ile çarpılarak toplamda kaç byte veri gönderileceği hesaplanır. Örneğin count = 16 ve MPI_INT'in büyüklüğü 4 byte ise toplamda 64 byte veri gönderilecektir. MPI'ın derlendiği makinanın mimarisine ve derleme parametrelerine bağlı olarak MPI_INT'in büyüklüğü değişebilir.

dest haberleşme dünyası içerisindeki herhangi bir işlemci numarasıdır. Sıfırdan MPI_Comm_size'ın döndürdüğü size değişkeninin bir eksiğine kadar herhangi bir değer olabilir. Bu değerden daha büyük yada sıfırdan küçük değerler programın yanlış çalışmasına ve çökmesine yol açabilir. Burada bir açıklama daha gerekiyor. MPI'ın bir standardı ve bu standartlara göre üreticiler tarafından yazılmış uyarlamaları (implementation) bulunmaktadır. Standartlar konunun ana hatlarını tamamlasa da bazı noktaları bilerek (örneğin esneklik açısından) yada bilmeyerek boş bırakmış olabilir. Bu boşluklardan biri kendine veri göndermeye çalışan program hakkındadır. MPI'ın Intel uyarlaması buna kesinlikle izin vermez ve program kendi kendine veri göndermeye kalkıştığında çöker. OpenMPI uyarlaması ise sorunsuz çalışır ve program kendi kendine veri gönderip alabilir. (En son hatırladığım bu şekildeydi. Sonradan uyarlamalar yada standartlar değişmiş olabilir.)

tag gönderilen mesajlara etiket vermeye yarayan bir sayıdır ve kaç olduğunun bir önemi yoktur sadece gönderme ve almada (MPI_Recv) aynı sayı olmalıdır. Karmaşık ve ardarda gönderilen birden fazla mesaj bulunduğu durumlarda işleri oldukça kolaylaştırır. Ve son olarak dest ile bildirilen işlemcinin hangi haberleşme dünyasına ait olduğunu bilebilmek için comm değişkeni haberleşme dünyasını tutar.


int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status): MPI_Send ile gönderilen veri bir başka işlemci tarafından MPI_Recv çağırılarak belleğe alınır. Bu fonksiyonda benzer biçimde buf ile belirtilen adrese count tane datatype türünde değişken source numaralı işlemciden tag etiketiyle comm haberleşme dünyası içerisinde okunur. MPI_Send'den tek farkı burada MPI_Status türünde bir durum döndürülür ancak genellikle bu değer MPI_STATUS_IGNORE değeriyle görmezden gelinir.

buf adresi MPI_Recv çağırılmadan önce bellekte ayrılmış olmalıdır. datatype, MPI_Send'in veri türüyle uyumlu olmak zorunda değilse de bunlar her zaman birbirinin aynı verilir. source yine gönderici işlemcinin sıra numarasıdır. Sıra numarasının bilinmediği yada fark etmediği durumlarda MPI_ANY_SOURCE kullanılabilir. Yine benzer şekilde etiket numaraları birbirini mutlaka tutmak zorundadır ancak etiket numarasının önemsenmediği durumlarda MPI_ANY_TAG kullanılabilir.


int MPI_Bcast( void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm ): MPI_Bcast fonksiyonu comm haberleşme dünyasında root numaralı işlemcinin buffer adresindeki count tane datatype türündeki veriyi diğer işlemcilere de kopyalar. Fonksiyon çağırılmadan önce bütün işlemciler buffer bellek adresini bellekte ayırmış olmalıdırlar.

http://www.mathcs.emory.edu/~cheung/Courses/355/Syllabus/92-MPI/group-comm.html

Peki bir MPI_Bcast çağrısının, 0'dan (size - 1)'e kadar olan bir for döngüsü içerisindeki MPI_Send'den farkı nedir? Yapılan iş aynı olmakla birlikte nasıl yapıldığı farklıdır. MPI_Send yaklaşımında haberleşme dünyasındaki işlemci sayısı kadar MPI_Send çağrısı gerçekleşir. Öte yandan MPI_Bcast'in çalışma prensibi şöyledir: Haberleşme dünyasında sekiz işlemci olduğunu ve root'u 0. işlemci olan bir MPI_Bcast çağrısı yapıldığını varsayalım. İlk adımda 0. işlemci buffer'ının tamamını 4. işlemciye gönderir. İkinci adımda 0. ve 4. işlemciler buffer'larının tamamını sırasıyla 2. ve 6. işlemcilere gönderirler. Son adımdaysa 0., 2., 4. ve 6. işlemciler buffer'larının tamamını sırayla 1., 3., 5. ve 7. işlemcilere yolladıkları zaman işlem üç adımda tamamlanır. Yani işlemci sayısı p olmak üzere MPI_Send ile p adımda yaptığımız işlemi MPI_Bcast bize log2(p) adımda tamamlamaya olanak sağlar. p sayısının en az 1 olacağını düşünürsek her durumda sayının logaritması kendisinden küçüktür. 

MPI_Bcast kullanırken dikkat edilmesi gereken bütün işlemcilerin MPI_Bcast fonksiyonunu çağırma zorunluluğudur. Yani fonksiyon, rank değişkeni işlemci numarasını göstermek üzere if(rank == 3) yada if(rank % 2) gibi bir ifade içerisinde çağırılamaz. Bunun yerine ikinci ifade için tek ve çift sayılı işlemcilerden oluşan yeni bir haberleşme dünyası tanımlanmalıdır.


int MPI_Abort(MPI_Comm comm, int errorcode): MPI_Abort çağrısı bir işlemcinin hatayla karşılaşması durumunda bütün haberleşme dünyasını sonlandırmak ve programdan çıkmak için kullanılır.


int MPI_Finalize( void ): Bu fonksiyon diğer işlemcileri öldürür. Genellikle main()'den çıkılmadan hemen önce kullanılır ve bütün MPI kodları bu fonksiyonla bitmek zorundadır. Bu fonksiyondan sonra yapılacak herhangi bir MPI fonksiyon çağrısı hataya sebep olur.


Son olarak MPI kurmak için bir hesaplama kümesi sahibi olmaya gerek yoktur. Herkes makinasına MPI kurabilir. Makinaların dağıtık olmasına gerek olmaksızın MPI tek bir makinadaki işlemcileri ve çekirdekleri ayrı işlemciler olarak etkin biçimde kullanabilir. MPI ile yazılmış kodları derlemek için, MPI kurulurken icc yada cc için kendi wrapper derleyicisini mpiicc yada mpicc adında kurar. mpicc kullanırken -L, -I gibi parametreleri ayrıca vermeye gerek kalmaz. MPI kodu mpi derleyicisiyle derlendikten sonra mpirun yada mpiexec komutlarıyla çalıştırılır. Örneğin mpirun -np 4 ./deneme.x biçiminde bir komut deneme.x çalıştırılabilir dosyasını 4 işlemcide çalıştırır. Çalıştırılan makinada 4 işlemci olmasa bile mpirun 4 işlemci varmış gibi çalışıp çıktı üretecektir. Ancak dört çekirdekli bir makinada programın 4 işlemciyle çalışma süresi 2 işlemciyle çalışma süresinin yarısı olacakken tek çekirdekli bir makinada herhangi bir fark görülmez hatta 4 işlemciyle daha uzun sürede çalışacaktır. MPI ile ilgili kod örneklerine burada değinmeyeceğim ancak internette bunlarla ilgili bir sürü örnek bulmak olası. Bir sonraki bölümde yayınlayacağım kod ve açıklamaları zaten bu fonksiyonlara ve bir önceki bölümde anlattığım fonksiyonlara bir örnek olacaktır.

23 Temmuz 2013 Salı

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


Yazıyı, birkaç kütüphaneyi anlattığım upuzun bir yazı olarak yayınlamaktansa üç ayrı bölümde yayınlamaya karar verdim. Böylesinin hem benim yazmam açısından hem de okurun takip edebilmesi açısından daha kolay olacağını düşünüyorum. Yazıda başlıkta da yazdığım gibi unrar kütüphanesini kullanarak ve MPI (Message Passing Interface) kütüphanesiyle birleştirerek kendi yazdığım paralel bir şifre bulucuyu anlatacağım. Ancak sonraki yazıları ne zaman yayınların bunu iş yoğunluğu gösterecek.


Bölüm1: Unrar kütüphanesi

Unrar kütüphesinin iyi bir dökümantasyonu yok ancak ben internette bulduğum bir belgeyi kısmen Türkçe'ye çevireceğim. Merak edenler bağlantıyı kullanarak belgenin orjinaline ulaşabilirler.

www.rarlabs.com'da linux için rar programı kaynak koduyla birlikte dağıtılıyor. Kodu satır satır incelemedim ama .rar dosyaları kişisel kodlarda kullanmak için Windows altında unrar.dll yada linux altında unrar.so adında rar açıcı (unrar) kütüphanesi geliyor. Ben kendi kodumda da bu kütüphaneyi kullandım. Kütüphane fonksiyonları kısaca şöyle:

  • RAROpenArchive: Prototipi,
HANDLE PASCAL RAROpenArchive(struct RAROpenArchiveData *ArchiveData)

şeklindedir. Değişkenlerin ve yapıların tanımları unrar paketindeki dll.hpp dosyasında ve yukarıda verdiğim linkte var. Yapıda iki tane önemli alan var. Birisi char *ArcName diğeri de UINT OpenMode. İlki açılacak arşivin adını ve yolunu tutuyor. Diğeri de hangi modda açılacağını. OpenMode için iki tane öntanımlı değer var. Birisi RAR_OM_LIST diğeri de RAR_OM_EXTRACT. Listeleme modu biraz daha hızlı ama adı üzerinde sadece listelemek için. Hata durumunda NULL handle döndürüyor. 

  • RARCloseArchive: Prototipi,
int PASCAL RARCloseArchive(HANDLE hArcData)

şeklindedir. RAROpenArchive'den alınan handle ile dosya kapatılır.

  • RARReadHeader: Prototipi,
int PASCAL RARReadHeader(HANDLE hArcData, struct RARHeaderData *HeaderData)

şeklindedir. Açılmış arşiv dosyanın içerisindeki ilk dosyanın başlığı okunur. Dosyalara ait başlık bilgileri önemlidir çünkü dosyanın şifrelenip şifrelenmediği bilgisi başlıktan alınır. Fonksiyon arşiv dosyası açıldıktan hemen sonra çağırılır. Eğer dosya adları şifrelenmişse fonksiyon ERAR_MISSING_PASSWORD hatasını döndürür. 

.rar dosyalarda iki tür şifreleme yapılabilir. Birisi dosya dosya şifreleme, diğeri de tüm arşivin dosya adlarıyla birlikte şifrelemesi. İlkinde istenirse bazı dosyalara şifre verip bazıları açık olarak saklanabilir. Bunun dezavantajı şifrelerin bulunabilir olmasıdır. Örnegin arşivin içerisinde "Alphaville - Big in Japan.mp3" bulunduğunu varsayalım. Şifreyi bulmak isteyen kişi aynı dosyayı internetten bulabilirse (dosya boyutundan aynı olup olmadığı anlaşılabilir) .rar içerisinde şifrelenerek sıkıştırılmış veriyi şifrelemeden sıkıştırılmış veriyle karşılaştırarak şifreyi bulabilir. Arşivdeki diğer dosyaların şifresi aynıysa diğer dosyalar da açılabilir. Bundan korunmanın yolu dosya adlarıyla birlikte tüm arşivi şifrelemektir. İlk durumda WinRAR ile açıldığında dosya adları listelenecek ve şifrelenmiş olanlar yanlarında * ile görüntülecek; ikinci durumda WinRAR ile açmaya kalkar kalkmaz şifre soracak, içeriği göstermeyecektir.

Arşiv içerisindeki her dosya için RARReadHeader çağırılır. Son dosyadan sonra bu fonksiyon çağırıldığında ERAR_END_ARCHIVE döndürür. Arşivin geneli değil de dosya dosya şifreleme yapılmışsa RARReadHeader hata döndürmez onun yerine RARHeaderData'nın Flags alanındaki bir biti set eder. Bir dosyanın başlığı okunduktan sonra RARProcessFile çağırılarak bir sonraki dosyaya geçilir.

  • RARProcessFile: Prototipi,
int PASCAL RARProcessFile(HANDLE hArcData, int Operation, char *DestPath, char *DestName)

şeklindedir. Başlığı RARReadHeader ile okunan dosyanın ne yapılacağı bu fonksiyonun Operation argumanıyla belirlenir. Operation için tanımlı değerler RAR_SKIP, RAR_TEST ve RAR_EXTRACT biçimindedir. Eğer dosya RAR_OM_LIST modunda açılmışsa bütün değerler RAR_SKIP'e eşdeğerdir.

.rar dosyaların .zip'lere göre önemli üstünlüğü katı arşiv (solid archive) oluşturulabilmesi. Arşivlerde dosyalar tek tek sıkıştırılabileceği gibi hepsi bir bütün olarak bir dosyada toplanıp sıkıştırılabilir. Linux'ta gzip de bzip de ayrı ayrı dosyaları sıkıştırmaz yalnızca karakter dizisini (stream) sıkıştırır. Dosyalar tek bir .tar dosya içerisinde birleştirilir (tar sıkıştırmaz) sonra gzip yada bzip ile sıkıştırılır. Benzeri katı arşiv .rar dosyalarda da geçerlidir. Katı arşivin avantajı biraz daha fazla sıkıştırabilir olmasıdır. Örneğin .jpg dosyalar zaten sıkıştırılmış olduklarından ayrı ayrı fazla sıkışmazlar ancak benzer header verilerine sahip .jpg'ler tek bir dosyada birleştirilirse benzer header'lar kolayca sıkıştırılır. Katı arşivin avantajı daha fazla sıkıştırılabilmesi olsa da arşivden bir dosya açmak için kendisinden önce gelen bütün dosyaların açılması gerekmektedir. 

Katı arşiv dosyaları RAR_OM_EXTRACT modunda açılmışsa RAR_SKIP verildiğinde kendisinden önceki dosyalar yine açılır, bu nedenle katı arşivlerde dosyalara bakmak biraz daha zaman alır. 

Eğer tek bir dosya şifrelenmişse açılmaya çalışıldığı zaman fonksiyon ERAR_BAD_DATA döndürür, oysa bu hata CRC hatasına karışılık gelir. Bunun açıklaması büyük olasılıkla şu şekildedir. Dosya sıkıştırılırken CRC'si hesaplanarak yazılır ancak sıkıştırılmış veri çıktısı girilen şifre üzerinden bir işlemden daha geçirilir. Basit olması açısından bunu XOR olarak kabul edelim. Sıkıştırılmış veri, açma (decompression) sırasında girilen şifre üzerinden XOR'lanıp sonra açma algoritmasına sokulur. Sonuçta çıkan verinin CRC'si eğer şifre yanlışsa kaydedilmiş CRC ile uyuşmayacaktır. Şifrenin ayrı olarak arşivin içerisinde kaydedilmeyip karakter dizisi üzerinden işleme sokulmasından dolayı .rar şifreleri yalnızca sözlük tabanlı saldırılarla bulunabilir. Hiçbir dosyasının bilinmediği bir arşiv dosyasın içeriğinden şifresinin bulunması olanaksızdır.

  • RARSetPassword: Prototipi,
void PASCAL RARSetPassword(HANDLE hArcData, char *Password)

şeklindedir. Bu şifreli arşivler için en önemli fonksiyondur. Kullanımı prototipinden anlaşılacağı üzere basittir. 

6 Temmuz 2013 Cumartesi

Eğrisiyle Doğrusuyla Debug.exe


(Bu yazıyı daha önce yazdığım ve kısıtlı bir çevrede dağıttığım bir belgenin küçük bir kısmından derledim. Internet'te bu yazının bazı bölümlerini aynen içeren bir doküman bulursanız o benim olabilir.)

debug.exe, DOS zamanlarından beri herkeste bulunan; Windows'la birlikte hala dağıtılan (32 bit Windows7'de var.) bir araç. Peki ne işe yarar bu araç? Nasıl kullanılır?

Bu araç elbetteki koddaki bug'ları otomatik temizlemiyor. Adından anlaşılacağı üzere bu bir debugger. Yani programın çalışmasını mikroişlemci düzeyinde inceleyip varsa hataları görmek, tek tek komut komut çalıştırıp kısmen de olsa değişiklikler yapmak olanaklı. Windows için güncel debugger'ları takip edemedim ama 2000'lerin başında Win32Dasm ve SoftICE kullanırdık. Bunlara da yeri gelirse kısaca değinirim ancak baştan belirteyim debug.exe DOS1.0 zamanlarında kalma (1980'lerin başından) bir program. 80286 komut setini bile desteklemiyor. EAX, EDI gibi yazmaçlara erişilemiyor. Daha iyi işler yapabilmek için daha güçlü bir debugger'a gereksinimi olanların TASM paketi içerisindeki Turbo Debugger'ı yada FreeDOS'un debugger'ini kullanmaları daha mantıklı.

Komut satırında debug dediğiniz zaman debug'un kendi iptidai - ile belirtilen komut satırına düşülüyor. Debug'dan sonra "debug \WINDOWS\system32\format.com" şeklinde dosya adı da vermek mümkün hatta format.com'un komut satırından aldığı parametreler de dosya adından sonra eklenebilir. Debug.exe'nin komut satırına düştükten sonra kendi tek harfli komutlarını bilmek gerekiyor. Bu komutlar komut satırında ? yazarak ulaşabileceğiniz bir avuç komut. Şimdi bunları sırasıyla açıklayayım.

* a (Assemble): a komutu girildiği zaman debug size CS:0100 (CS: Code Segment. Kodun bellekteki adresini tutan bir yazmaç. Dolayısıyla siz a yazdığınızca CS'nin içeriği kaçsa o sayıyı gösterecek. Bunu işletim sistemi 640Kb'lik bellekteki boş alana göre ayarlar.) adresini gösterir ve sizden x86 assembly dili komutlarını girmenizi bekler. 0100h offset adresi de DOS'ta bir .com çalıştırılabilir dosya yüklendiğinde onun ilk komutunun bulunduğu yerdir. Segmentin 0000h ile 0100h arasında PSP (Program Segment Prefix) adı verilen başlık bulunur. Komut yazma modunda komut yazmadan enter'a basınca çıkılır. a'dan sonra herhangi bir 16 bitlik sayı girilirse komutun girilmesine o satırdan devam edilir.

-a0200
1381:0200 mov bx,b800
1381:0203


* c (Compare): c komutu iki bellek parçasını karşılaştırmak için kullanılır. Yalnızca farklı olan byte'lar gösterilir. İstenirse farklı segmentler de verilebilir. Aşağıdaki örnek 0000:0100h ile 0000:0110h arası parçayı DS:0400h'den itibaren karşılaştırır.

-c 0000:0100 0110 0400
0000:0100  BB  00  1381:0400

0000:0102  B8  00  1381:0402
0000:010A  01  00  1381:040A 
0000:010B  1A  00  1381:040B

* d (Dump): d komutu belleğin bir parçasını görüntüler. Tek parametreyle verilirse segmenti DS olarak seçer  ve 128 byte görüntüler. İki parametreyle verilirse verilen iki offset arasını gösterir. Parametresiz verildiğinde en son görüntülediği bellek parçasının ardışığı 128 byte'ı gösterir. Böylece ardarda verilen d komutlarıyla uzun bir bloğa parça parça bakmak olanaklıdır.

-d0110 011F
1383:0110  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00   ................



* e (Enter): e komutuyla bir bellek bölgesine elle byte'lar iki türlü girilebilir:

Aşağıdaki komutla DS:0120h'den itibaren 13h, 16h, 19h ve 1Ch byte'ları sırasıyla yazılır.
-e 0120 13 16 19 1C

Aşağıdaki komutla ES:0200h'dan itibaren 13h, 16h, 19h ve 1Ch byte'ları tek tek yazılır. Sayılar girildikten sonra boşluk karakterine basmak gerekir:
-e ES:0200
1381:0200  00.13   00.16   00.19   00.1C

* f (Fill): f komutu bellek bölgesini verilen karakterlerle belli uzunlukta doldurur.

Aşağıdaki komutla 0100h offsetinden başlayarak 12h, 13h, 14h byte sırası 16 byte boyunca tekrarlanır:
-f 0100L10 12 13 14

* g (Go): g komutu, CS:IP ile gösterilen bellek adresinde hangi komutlar varsa koşulsuz olarak çalıştırır. Program sonlanıp yada kesilip çalışmayı debug.exe'ye geri bırakana kadar sürer. g'den sonra bir bellek offset adresi verilirse debug onu verilen bellek adresine kadar çalıştırıp ardından r komutunu uygular ve tekrar debug.exe'nin komut satırına düşer. Bu bellek adresine breakpoint denir. Debugger'larda breakpoint'ler belirlenerek program parça parça çalıştırılabilir. g komutuna ayrı bir parametre vermek yerine çalışması kesime uğratılacak noktadaki kod parçası 0CCh (INT 3) ile değiştirilebilir.

g komutu için söylenmesi gereken önemli bir şey daha var: Eğer program DOS 1.0 zamanlarından kalma INT 20h kesmesini kullanarak çıkıyorsa debug.exe "Program terminated normally." diyor ve program sorunsuz debug.exe'ye düşüyor. Yanılmıyorsam bu INT 21h,00h için de geçerli. Ancak DOS 2.0 ve sonrası için önerilen program sonlandırma yöntemi INT 21h,4Ch çağırıldığı zaman bu yöntem debug.exe'yi de sonlandırıp DOS komut satırına düşmenize neden olabiliyor (özellikle birden fazla kere çağırıldığında).

* h (Hex): h komutu verilen iki sayıyı toplar ve çıkarır.

-h 2D1 2E8
05B9  FFE9


* i (Input): Bir porttan veri okur. Komutu port numarası takip eder. Windows NT tabanlı tüm işletim sistemlerinde bu veriye pek güvenmemek gerekiyor. Doğrudan portlara erişmek bazı durumlarda olanaksız, örneğin Windows'un diski kontrol eden sürücüsü diske erişmenize izin vermez. Basit görece zararsız portlar (klavye gibi) işletim sistemi üzerinden erişilebilir durumdalar.

-i 60
1C


60h portu klavye denetleyicisinin portudur.

* l (Load): l komutunun iki kullanımı bulunmaktadır. Birinci kullanımda mantıksal bir sektörü (cluster) belleğe okumakta kullanılır. Bunun kullanımı Windows NT tabanlı sistemlerde yine kısıtlı. XP'de yanlış hatırlamıyorsam çekirdek sürücüleri üzerinden okuma yapılabiliyordu, yazmaya izin yoktu. Windows 7'de şöyle şeyler oluyor:

Bu diyalog kutusunda Close diyince debug.exe sonlandırılıyor, Ignore diyince "Disk error reading drive C" hatası veriyor.

Komutun kullanımında, l'den sonra gelen parametre cluster'ın okunacağı bellek adresi, 02h disk sıra numarası. ( 00h = A: , 01h = B:, 02h = C:, 03h = D: ... ) Sonraki sayılar cluster numarası ve okunacak toplam cluster sayısı.

İkinci kullanımında n ile belirtilen dosyayı (çalıştırılabilir olması şart değil) CS:0100h adresine yükler. Eğer dosya bir .exe dosya ise yüklenecek yer CS:0100h olmayabilir. Bu kullanımla ilgili n komutunun açıklamasına da bakılabilir.

* m (Move): m komutu bir bellek parçasını başka bir bellek parçasının üstüne kopyalar.

DS:0200h'dan DS:0400h'a 128 byte kopyala:
-m 0200 l80 0400

* n (Name): n komutu bir dosya yada parametrelerini debug.exe'ye bildirmeye yarar. l ile bir dosya okumak yada w ile bir dosya yazılmak istendiğinde önce n komutuyla dosya bildirilmesi gerekmektedir. Komut satırında debug'dan sonra dosya adı verilebileceğini belirtmiştim. Eğer bu dosya adı komut satırından verilmezse n komutuyla verilip sonra l komutuyla dosya yüklenebilir. n komutu dosyanın olup olmadığını kontrol etmez, yalnızca bir FCB (File Control Block) oluşturur. l komutu da bu FCB'deki dosyayı açmaya çalışır. Dolayısıyla n komutuna ne girilirse girilsin hata vermeyecektir. İlgili hata l komutu verildiği zaman alınır.

-n\Windows\winhlp32.exe
-l


Yazmaya ilgili bilgiler w komutunda daha etraflıca anlatılacaktır.

* o (Output): Bir porta veri yazar. i komutu gibi bunda da komutu port numarası takip eder. Port numarasından sonra porta gönderilecek veri yazılır. Yine Windows NT tabanlı tüm işletim sistemlerinde i komutundaki gibi güvenlik vb. nedenlerden bu komutun sonucuna pek güvenmemek gerekiyor. Veri porta gönderilmemiş olabilir yada güvenlik hatasıyla debug.exe sonlandırılabilir.

71h portu CMOS adresleme, 70h portu da okuma portudur. Aşağıdaki komutlarla CMOS'un 19h byte'ı okunur:
-o 71 19
-i 70
DD

* p (Proceed): p komutu tek başına kullanıldığında CS:IP'teki bir komutu çalıştırır ancak eğer CS:IP'deki komut bir altprogram yada kesme çağrısıysa (CALL yada INT) içine girmeden tamamını çalıştırılır ve çalışmanın sonunda yazmaçların içeriklerini gösterir (r komutunun çıktısı).

p komutundan sonra bir sayı girilirse (p 3 gibi) debug.exe o kadar sayıda komutu çalıştırarak tek tek her seferinde yazmaç içeriklerini gösterir.

p komutundan sonra başında = bulunan bir offset adresi girildiği zaman IP (Instruction Pointer) bu offset adresine gelinceye kadar program çalışır. O adreste komut bitmiyorsa komutun bittiği son yerde çalışma durur. Yani IP'nin son değeri bu değere eşit yada büyüktür.

-p =0107

AX=0900  BX=FFFF  CX=FE00  DX=0000  SP=00B8  BP=0000  SI=0000  DI=0000
DS=13DA  ES=13DA  SS=13EA  CS=13EA  IP=0107   NV UP EI PL NZ NA PO NC
13EA:0107 CD21          INT     21


* q (Quit): q komutu debug.exe'den çıkar. Herhangi bir parametresi yoktur, verilse de dikkate alınmaz. Yani q, quit yada quake yazılsa da debug.exe'den çıkar.

* r (Register): r komutu yazmaç içeriklerini görüntülemek ve değiştirmek için kullanılır. r komutu tek başına bütün yazmaçları görüntüler.

-r
AX=0900  BX=FFFF  CX=FE00  DX=0000  SP=00B8  BP=0000  SI=0000  DI=0000
DS=13DA  ES=13DA  SS=13EA  CS=13EA  IP=0007   NV UP EI PL NZ NA PO NC
13EA:0007 CD21          INT     21

Çıktıyı kısaca açıklarsam, AX, BX, CX ve DX x86 serisi işlemcilerin çeşitli komutlarla kullandığı genel amaçlı yazmaçlardır. Accumulator, Base, Counter, Data gibi açılımları olsa da ( http://en.wikipedia.org/wiki/AX_register#Purpose ) genel olarak bunların içeriklerini değiştirmenin komutlar işlemediği sürece bir etkisi yoktur.

SP, stack pointer anlamına gelir ve işlemcinin o anda kullandığı stack'in (yığın) offset adresini tutar. BP, base pointer anlamına gelir ve göstericilerin tutulması için yardımcı bir yazmaçtır. Genelde argümanlarına yığın üzerinden erişen C gibi yüksek seviyeli dillerde fonksiyonların en başında şuna benzer bir kod bloğu bulunur:

k = fonk1(0x0202, 0x0101);

karşılığında üretilen kod:

mov ax, 0101
push ax
mov ax, 0202
push ax
call @fonk1

@fonk1:
push bp
mov bp,sp
...
pop bp
ret

biçimindedir. Alt program blogunda kısa (segment içi) çağrı için ilk arguman [bp+04] ikincisi [bp+06] ve uzun (segment dışı) çağrı için ilk arguman [bp+06] ve ikincisi [bp+08] adresinde bulunur. Yani alt programda BP yedek bir SP işi görür ama elbette ki bu bir zorunluluk değildir. SP genelde doğrudan değiştirilmez, push ve pop gibi komutlarla değeri kendiliğinden değişir.

SI, source index ve DI destination index anlamına gelir. MOVS, STOS, SCAS ... gibi karakter dizisi üzerinden yapılan işlemlerde okumalar DS:SI üzerinden, yazma işlemleriyse ES:DI üzerinden yapılır.

DS, data segment anlamına gelir. Genelde veriler üzerinde yapılacak işlemlerde verilerin tutulduğu segment DS'de bulunur. CS, code segment anlamındadır. CS, IP (instruction pointer) yazmacıyla birlikte mikroişlemcinin o an çalıştıracağı komutu tutar. SS'in anlamı stack segment'tir adından da anlaşılacağı üzere SP ile birlikte yığını gösterir. Son olarak ES, extra segment'dir ve bütün bu segment yazmaçlarının haricinde bir yazmaça ihtiyaç duyulması durumunda yardımcı segment yazmacı olarak kullanılır. IP yazmacı programcı tarafından yalnızca jmp ve koşullu dallanma komutlarıyla değiştirilebilir. Her komutun çalıştırılması sonucunda bir sonraki komutu gösterecek şekilde mikroişlemci tarafından değiştirilir. Örneğin 2 byte'lık bir komutun çalıştırılması sonucunda değeri 2, 3 byte'lık bir komutun çalıştırılması sonucunda değeri 3 arttırılır.

NV UP gibi ifadeler FLAGS yazmacına aittir. Bununla ilgili Temmuz 2012 tarihinde "Peki Neden Trap Flag?" başlıklı yazıda ayrıntılı bilgi bulunuyor. En alt satırdaysa o anki CS:IP'nin değeri ve o değerin gösterdiği yerdeki mikroişlemci komutları ve bunların Assembly karşılığı bulunur.

r komutuyla yazmacın içeriğinin değiştirilmesi için r'den sonra yazmacın adı yazılır. Enter'a basıldığında yazmaçtaki o anki değer görüntülenir ve ardından yeni değer girilmesi beklenir. Herhangi bir değer girmeden enter'a basılırsa yazmacın içeriği değişmez. FLAGS yazmacının içeriğine rf komutuyla erişilebilir ve içerik bu komutla değiştirilebilir.

-rax
AX 0202
:0303


* s (Search): s komutu başlangıcı ve bitişi verilen bir aralıkta verilen bir byte yada byte dizisini aramaya yarar.

Aşağıdaki komut DS:0100h ile 0200h arasındaki bütün 0b8h 00h 01h dizisini listeler:
-s 0100 0200 b8 00 01
1381:0100
1381:01A4


İstenirse aranacak veri çift tırnak içerisinde belirtilerek karakterle arama da yapılabilir:
-s 0100 0200 "h"

* t (Trace): t komutu da p komutu gibi tek başına kullanıldığında CS:IP'teki bir komutu çalıştırır ancak p'den farklı olarak t komutu, CALL yada INT gibi komutların içerisine girerek onları da çalıştırır. Çıktı olarak p gibi yazmaç içeriklerini gösterir. t komutu da p komutu gibi komut sayısı belirtilerek yada başında = olan bir offset adresiyle kullanılabilir. Parametrelerin etkisi de p komutuyla aynıdır.

* u (Unassemble): u komutu belleğin bir parçasındaki byte'ları alarak bunları Assembly kodlarına geri çevirir ve ekrana sığdığı kadarıyla listeler. Eğer tek parametreyle verilirse bu değeri offset değeri olarak alıp listelemeye bu offsetten başlar. İki parametreyle verildiğinde birinci değer başlangıç ikinci değer bitiş olmak üzere aradaki kodlar listelenir. Parametresiz verildiğinde en son görüntülediği kod bloğunun bitişiğindeki bloğu listeler. Ardarda verilen u komutlarıyla uzun bir bloğa parça parça bakmak olanaklıdır.

* w (Write): w komutunun da l komutu gibi iki kullanımı bulunmaktadır. l ye benzer olarak bellekten mantıksal sektörlere yazılabilir. (Windows NT de kısıtlamalar var.) Bu kullanımda parametreler l komutuyla aynıdır.

Dosya yazdırılacaksa yine n komutuyla birlikte kullanılır, kaç byte yazılacağı da BX:CX yazmaçlarında tutulur. Yazılacak verinin büyüklüğü 16lık sayı sistemine çevirilir. Düşük değerli 4 basamak CX'te yüksek değerli 4 basamak BX'de olmak üzere 32 bite kadar büyüklükte dosyalar yazılabilir.

Son olarak bunlardan başka X ile başlayan EMS ile ilgili komutlar bulunsa da EMS konusunda bilgim olmadığından bunlara değinmeyeceğim.


Dipnot: Yeni yazı maalesef yine çok uzunca bir zaman gecikti. Mayıs ortasında diskim bozuldu, sonrasında önemli bir sınava girdim ki yedekleme, işletim sistemi kurma, geri yüklemeyle ilgilenmekten sınava doğru dürüst çalışamadım bile. Haziran başında ülkenin içerisinde bulunduğu siyasi durumu izlemekten iş yerinde dahi doğru dürüst çalışamadım. Haziran ortasında bir daha diskim bozuldu falan filan derken Nisan'ın 13ünde başladığım yazıyı tamamlamam bugünü buldu. Gecikmeden ötürü herkesten özür dilerim. 

13 Nisan 2013 Cumartesi

Bir başka PIC #2


Geçtiğimiz hafta müsvedde kağıtlarımın arasında "PNP kağıdıyla baskı devre yapımı" başlıklı yazıda yaptığım devrenin şemasını buldum ve oturup yaptığım devrede arızanın nerede olduğuna bakmaya başladım. Buna en son değinirim. Önce asıl yazacağım konuyu ele alayım.

Yazı esas olarak Temmuz 2012'deki PIC yazımın devamı gibi olacak. Temmuz ayında kendi programlayıcımı yaptıktan sonraki aşamalara kısaca değineceğim.

Temmuzda yaptığım programlayıcı seri arabirimi kullanıyordu. Maalesef artık masaüstü makinalarda bile seri arabirim kalmadı. Seri arabirimi, yönlendirici (router), switch ve disk denetleyici ayarlarken hala kullanıyorum ama acil bir durumda bile kendi dizüstü bilgisayarıma USB-Seri çevirici takıp onu kullanmam gerekiyor. Bu çevirici için ayrıca bir sürücü gerekli. Ve sürücü programlayıcı için bilgisayar tarafında kullanan programla (IC-Prog gibi) uyumlu çalışmadığından, seri PIC programlayıcıyı kullanabilmek için Windows 98'i olan ayrı bir makina bulmak gerekiyordu. Bu da hiç pratik değil.

Yazıcıoğlunda gezerken BioPIC USB Programlayıcı'yı daha önce görmüştüm. 


USB kullandığından programlaması pratik. Üstelik programlayıcının üzerindeki USB iletişimi sağlayan bir PIC18F2550. Artısı seri programlayıcıya göre pratik olması, dış güç kaynağına gerek olmaması ve onlarca PIC serisi denetleyiciyi desteklemesi. Eksisi diyemeyeceğim ancak çeviriciler gibi bilgisayara bir sürücü kurmak gerekiyor. Satın aldıktan sonra CD'de Usburn yazılımıyla geliyor.

Programlayıcıyı bilgisayara takınca ışıklar kısa bir yanıp sönecek. Usburn'u çalıştırınca "Detect Programmer" yazıyorsa sürücülerde bir sorun vardır. "Identify PIC programmer" düğmesi çıkmalı. Buna tıklayınca artık .hex dosyaları okumak ve yazmak olanaklı. Internette bulunabilecek örnek bir .hex dosyayla denemeler yapılabilir. Daha önce denediğim arabirim programlarına göre üstün bir özelliği disassembler ve hex dump'ı da göstermesi. Programlayıcı bazen bilgisayara takıldığında ışıkları yanmadığında yada .hex dosya yazılıp doğrulaması bittikten hemen sonra sürekli ışığı yanıyorsa artık bilgisayar onu görmüyor. USB'yi çıkarıp tekrar takınca sorun düzeliyor.

Elbette ki bir derleyici olmadan sadece başkalarının .hex dosyalarını yazmak bir yerden sonra anlamsız. Benim amacım yaktığım PIC'deki kodu sıfırdan yazmaktı. Bu arada PIC assembly'si oldukça basit. Kendim anlatacak kadar değil ancak idare edecek kadar biliyordum, onu da kullanmaya kullanmaya çoğunu unuttum. O nedenle burada anlatamam ancak piyasada bunu anlatan kitaplar var. Sadece komut seti ve iç yapısı microchip firmasının sayfasındaki "datasheet"inden bulunabilir. O güne kadar PIC doğru düzgün Assembly kodu yazmamıştım. Gidip microchip'in sayfasından MPLAB IDE'yi indirdim. Bu bir IDE ancak içinde hazır bazı derleyici modüllerle geliyor ve bunları seçebiliyorsunuz. Üstelik ICE denen "In Circuit Emulation" özelliğiyle programlayıcıyı tanıyorsa ve destekleniyorsa IDE'den doğrudan programlayıcıya hex kodu aktarmak ve emülasyonunu yapmak olanaklı. Ben bu kadar gelişmiş özelliklerini ne biliyorum ne de kullanabiliyorum. Zaten bildiğim kadarıyla BioPIC'in böyle bir desteği de yok. 

Bu programı kullanılabilir hale getirmek biraz karışık olduğundan ayrıntılı anlatma ihtiyacı duyuyorum. MPLAB'ı kurup IDE'yi açtım. IDE'de doğrudan dosya seçmek yerine önce bir proje oluşturmak gerekiyor. Project -> New diyip proje için bir ad bir de dizin seçiyorum. Karışıklık olmasın diye bütün kodları bu dizinde tutacağım ama bu zorunlu değil. Tamam'a tıkladıktan sonra proje ve çıktı pencereleri çıkması lazım. Çıkmıyorsa View'dan gösterilmesi sağlanabilir. Proje penceresinde 'Source Files'a sağ tıklayıp 'Add Files' kaynak kodu seçip eklemek gerekiyor. Ekledikten sonra proje penceresinde dosya görünecek. Kaynak kod penceresi çıkmıyorsa dosyaya çift tıklayınca çıkacaktır. 

Burada Make yada Build demeden önce yapılması gereken önemli işler var. Birisi Project menüsü altında 'Select Language Toolsuite'den "Microchip MPASM Toolsuite" seçili olmalı ve elbetteki yüklü olmalı. Bu araç MPLAB IDE ile geliyor diye hatırlıyorum ama yüklü değilse yüklemek gerekir. Yüklüyse derleyici, linkleyici gibi ıvır zıvırların yollarının doğru bir biçimde belirtilmesi lazım, yani 'Toolsuite Contents' altında bunların karşısında kırmızı bir çarpı bulunmamalı. Resimde, benim bilgisayarımda MPASM kitinin varsayılan kurulumda nerede bulunduğu görülüyor. Bu ayarlar sanıyorum Project -> Set Language Tool Locations altında da bulunabilir. Uygun derleyiciyi seçtikten sonra Ctrl + F10 ile yada Project -> Build All diyerek kodu derliyorum. Kodun tamamını vermek gereksiz olacak ancak kodun başlığı şu şekilde:

;*************************************************************
    title    "zamanlayici"
;*************************************************************
    list    p=16F84A
    #include    <p16F84A.inc>
    __CONFIG    _CP_OFF & _WDT_OFF & _PWRTE_ON & _XT_OSC
    ERRORLEVEL -302

SaH        equ        0x10
SaL        equ        0x11
;*************************************************************
        org        0x0000
        goto    BASLA
...


Buradaki #include<p16F84A.inc> ifadesi önemli. Kullanılan sabitlerin birçoğu bu dosya eklendikten sonra tanımlanıyor. Diğer önemli bir ifadeyse __CONFIG makrosu. Bu, programlama sırasında denetleyicinin ayar yazmacına yazılacak olan 'word'u belirtiyor. Buradaki örnekte CP (Code Protection) kapalı, WDT (Watchdog Timer) kapalı, PWRTE (Power-Up Timer) açık ve kristal osilatör seçili. Kodda ve donanımda herşey doğru olsa bile buradaki bitlerden herhangi biri yanlışsa devre hiç çalışmayabilir. Bütün bu tanımlar ve daha bir çoğu (STATUS, PORTA, PORTB, TRISA, TRISB, ...) 16F84A'ya özgü olacak şekilde p16F84a.inc dosyasında bulunur. 

Eğer kodda bir hata yoksa sorunsuz derlenmesi ve .hex dosyanın oluşması gerekiyor. Eğer oluştuysa bundan sonrası .hex dosyayı Usburn'da açıp ROM'a yazdırmak oluyor. Elbette geliştirme aşamasında her seferinde kodda ufak hataları düzeltmek, sonrasında ROM'a yeni kodu yazıp mikrodenetleyiciyi programlayıcıdan sökmek ve devre kartına takıp devre kartında test etmek ve her hatada bunu tekrarlamak bir yerden sonra işkenceye dönüşebilir. ICE'nin avantajı burada devreye giriyor.

Herşey buraya kadar iyiydi. Kodun başındaki ayarları yapıp belleği ayarladım. Kodun altmış küsuruncu satırında bir kere daha hatırladım ki 16F84A komut setinde bölme komutu yok. Benim hem saniyeler için 60'a bölme ve kalan (modulo) hem de basamaklar için 10'a bölme ve kalan koduna ihtiyacım vardı ve açıkçası bölme kodunu ayrı ayrı yazmak istemiyordum. Tek bir bölme fonksiyonuysa beni daha da uğraştıracak gibi görünüyordu. Üstelik yığın (stack) yoktu, fonksiyona argüman göndermek ayrı bir sıkıntıydı. Bu nedenle kodu PIC C'de yazmaya karar verdim çünkü PIC C gibi bir araç varken Assembly'de uğraşmak kılıç kalkanla tanklara saldırmak gibi olacaktı.

Yine hatırladığım kadarıyla MPLAB'le birlikte bir C derleyicisi gelmiyor ama varolan C derleyicilerine destek veriyor. Zaten 'Select Language Toolsuite' altında bir çok C derleyicisi için ayar var. Yalnız varolan C derleyicileri paralı veya bedava ama kısıtlı özellikleri var. Ben iki tane C derleyicisi tavsiye edeceğim. Birisi MPLAB'ın XC8 derleyicisi, diğeri de HI-TECH C derleyicisi. Derleyicileri kurmadan önce hangi mikrodenetleyiciler için olduklarına dikkat edilmeli. Ben bu derleyiciler için bir gecemi ayırdığımdan yarı uykulu halde 16 bit, 32 bit ne varsa kurup çok sonradan aslında 8 bitlik derleyiciye ihtiyacım olduğunu anlamıştım. Hi-Tech C için yazılan C kodlarında Assembly'de eklenen dosya gibi htc.h adında bir başlık dosyası var. Nasıl ki neredeyse bütün C kodlarının başında #include<stdio.h> varsa Hi-Tech C'de de #include<htc.h> bulunuyor. __CONFIG makrosu yine var ama kullanımı Assembly'dekinden biraz daha farklı. Bir de #define _XTAL_FREQ 4000000 demek gerekiyor ki, zamanlamayla ilgili komutlar (__delay_ms() gibi) doğru çalışabilirsin. Elbetteki o sayı yerine devredeki kristal osilatör frekansı neyse o yazılmalı. Ben çoğunlukla Hi-Tech C'yi tercih ediyorum. Burada yine C kodlamayı yada PIC C'nin gündelik hayatta kullandığımız C'den farkını anlatamayacağım ancak bunun için 320volt.com'daki bir yazıyı önereceğim. Önbilgiye sahip okuycular yazının sonunda ek olarak verdiğim örnek kodu da inceleyebilirler. Devre kartının ayrıntısını vermeyeceğimi söylemiştim ancak kodun tamamını kendim yazdığımdan bunu yayınlayacağım.

Herhangi bir C derleyicisi kurup 'Set Language Tool Locations' altında gerekli çalıştırılabilir dosyaları gösterdikten sonra yapılacaklar aslında Assembly kodu derlerken yapılanlarla aynı. Elbette ki farklı derleyicilerin çıktıları farklı olacak. Ben aşağıdaki çıktıya dikkat çekmek istiyorum. 


Bu çıktı devresini kurduğum sayaç kodunun derleyici çıktısı. Gerçekten de derleyici sorununu halledip internetteki örneklere bakarak PIC C'yi çözdüm, kodu yazmaya başladım ve bir kaç saat sonra kodu bitirdim. O gece sabah ezanından sonrasına kadar çalıştım, sorunun çoğunu halletmiştim. İş yerine o gün gelemeyeceğimi e-postayla bildirip yattım. 4-5sa uykudan sonra kodun geri kalanını da bitirdim, denemeleri gerçekleştirdim ve herşey hazırdı.

Çıktıya geri dönersek; anlaşılacağı üzere derleme tamamlanmış ve şöyle birşey demiş: "The HI-TECH C PRO compiler output for this code could be 362 words smaller.". Yani derleyicinin paralı sürümü optimize kod üretiyor ve 906 word'de 362 word'luk bir küçülme aslında hiç de azımsanamayacak bir oran.

Önceki yazıda devreye oldukça hakim olduğumu belirtmiştim. Sayıcıyla ilgili herşeyi bitirip tekrar devreyi incelemeye geri dönünce aslında devreyi sadece sayıcı olarak kullanmanın haksızlık olacağını gördüm. Devre 3 tane giriş (klavye), bir tane gösterici dekoderi (ekran kartı) üzerinden sürülen 4 basamak 8 segment gösterici çıktısı (monitor), bir tane de röle çıktısı (port) olarak düşünüldüğünde aslında küçük bir bilgisayar olarak görülebilir. O halde devre kartı bir toplama bilgisayarı olarak yeniden programlanabilirdi. Şöyle ki iki düğme 00..99 arası sayıları arttıracak, bu sayılar göstericinin sağ iki ve sol iki basamağında gösterilecek ve üçüncü düğmeye basıldığında bu sayıların toplamı göstericide gösterilecek. Günün geri kalanında bu kodu da yazıp bitirdim ki aslında bu kodu yazmak sayıcının kodunu yazmaktan daha kolaydı. Göstericiyle ilgili kodlar zaten hazırdı. Bunun kodunu da yazının sonunda ek olarak vereceğim. Elbette ki basit bir sayıcı devresi, yeni birşeyler geliştirmek için çok da uygun değil. Bunun yerine daha genel amaçlı, geliştirmeye çok daha fazla açık PIC geliştirme kartları da elektronikçilerde bulunuyor. Örneğin aşağıdaki gibi:

Toplayıcıyı da bitirdikten sonra sayıcının kodunu biraz daha geliştireyim, örneğin dakika ve saniye ayarlama modundayken dakika ve saniyeler yanıp sönsün, fırın saati gibi görünsün istedimse de yukarıdaki çıktıda da görüldüğü üzere program ROM'unda 118 word kalmıştı. Dolayısıyla ikinci sürüm kodun bir kısmını tamamladım ama ROM'a sığmadı. Oysa PRO sürümünde optimizasyonlar açık olsaydı en az 362 word daha olacaktı ve çok rahatlıkla gelişmiş sürümü de bitirebilecektim. Bundan sonra da bu devreyle ilgili geliştirmeyi sonlandırdım.

Yazının en başında geçen ay yaptığım çift taraflı baskı devrede arızanın nerede olduğuna bakmaya başladığımı yazmıştım. Bir yandan bu böyle olmaz diyerek UT603 Sığa - Bobin Ölçme cihazını da sipariş ettim, yeni oyuncağım da kargoda. Devre kartında bir tanesi soğuk lehim kaynaklı ve 3 tanesi tasarım hatası kaynaklı arıza buldum. Tasarımda iki tane veri yolunu unutmuştum. Lehimleri tazeleyip iki tane veri yolunu dışarıdan tel lehimleyerek hallettim. Son olarak röle atmıyordu. Röleyi süren transistörü de söktüm ancak onu sökünce sadece kapasite modunda çalışan devre yeniden çalışmamaya başladı. Transistör bozuk değilmiş ancak sökerken büyük olasılıkla bozdum. En son durumda en azından kendisini açılışta kalibre etmeye çalışıyordu:


Ekler:


/*    16F84A entegreli zamanlayici devresi icin PIC C kod dokumu
    19.06.2012   */

#include 
#define _XTAL_FREQ 4000000

__CONFIG(CP_OFF & WDTE_OFF & PWRTE_ON & FOSC_XT);

int counter = 0;
int onesec = 0;        // bir saniye flag
char sayimda = 0;      // gerisayim flag
char cursec = 0;
char curmin = 0;
char digit1, digit2, digit3, digit4;

void guncelle();

void interrupt isr()    {       // kesme hizmet programi
    if(T0IF == 1)    {          // zamanlayici kesmesi mi?
        counter++;
        if(counter >= 1953)  {  // 4Mhz icin 1953 kesme 1sn olur.
            onesec = 1;
            counter = 0;
        }
        if(onesec == 1)    {
            onesec = 0;         // reset flag
            if(cursec == 0)  {  // saniye tasmasi
                if(curmin > 0)    {
                    cursec = 59;
                    curmin--;
                }
                else    {
                    sayimda = 0; // sayimi durdur
                    T0IE = 0;    // kesme'yi kapat
                }
            }
            else    {
                cursec--;
            }
            guncelle();          // basamaklari guncelle
        }
        T0IF = 0;
    }
}

void guncelle()    {
    digit4 = curmin / 10;        // en soldaki basamak
    digit3 = curmin % 10;
    digit2 = cursec / 10;
    digit1 = cursec % 10;        // en sagdaki basamak
}

void goster()    {
    char temp = PORTA & 0x10;
    /* PortA'nin 4. bitinde role bagli. 0-1-2-3'de 7 segment

       LED'leri adresliyor. Role'nin durumunu degistirmeden

       LED'leri taramak icin temp'de onceki PortA degerini

       yedekle. */

    PORTB = digit1;
    PORTA = temp | 0x01;
    __delay_ms(2);
    PORTA = temp;
   
    PORTB = digit2;
    PORTA = temp | 0x02;
    __delay_ms(2);
    PORTA = temp;

    if(counter < 976)   PORTB = digit3;
    else                PORTB = digit3 | 0x10;
    /* PortB'nin 4. bitine 7 segment'in noktalari bagli. Eger

       saniyenin ilk yarisiysa tam ortadaki 3. noktayi yakma ama

       ikinci yarisiysa yak. boylece saniyede bir kere yanip 

       sonecek. */ 


    PORTA = temp | 0x04;
    __delay_ms(2);
    PORTA = temp;

    PORTB = digit4;
    PORTA = temp | 0x08;
    __delay_ms(2);
    PORTA = temp;
}

void klavye()    {
    if(RB7 == 0)    {    // ucuncu dugmeye bagli.
        if(sayimda == 0)    {
            sayimda = 1;
            T0IE = 1;    // sayimda degilse kesmeyi ve sayimdayi 

                         // baslat
        }
        else    {
            sayimda = 0;
            T0IE = 0;    // sayimdaysa kesmeyi ve sayimdayi kapat
        }
        while(RB7 == 0)    goster();
        // basili tutuldugu sure icerisinde sadece ekrani tazele
    }
   
    if((RB6 == 0) && (sayimda == 0))    {
        // ikinci dugmeye bagli. sadece sayimda degilse aktif
        cursec++;          // saniyeyi arttir
        if(cursec == 60)   cursec = 0;        // saniye tasmasi
        guncelle();        // artmis saniyeyi guncelle
        while(RB6 == 0)    goster();
        // basili tutuldugu sure icerisinde sadece ekrani tazele
    }

    if((RB5 == 0) && (sayimda == 0))    {
        // birinci dugmeye bagli. sadece sayimda degilse aktif
        curmin++;          // dakikayi arttir
        if(curmin == 100)  curmin = 0;        // dakika tasmasi
        guncelle();        // artmis dakikayi guncelle
        while(RB5 == 0)    goster();
        // basili tutuldugu sure icerisinde sadece ekrani tazele
    }
}

void role()    {
    if((cursec == 0) && (curmin == 0))  RA4 = 1;
    // saniye ve dakika sifir oldugunda roleyi ac
    else                                RA4 = 0;
    // degilse roleyi kapat.
}

void main()    {

    TMR0 = 0;
    GIE = 1;           // kesmeleri aktiflestir
    T0IE = 0;          // zamanlayici kesmesini kapat
    OPTION_REG = 0;    /* PortB pull-up enable. Timer0 clock

                          source = CLKOUT. Prescaler to Timer0. 

                          Prescaler 1:2  */
    RBIF = 0;          // PortB kesmesini kapat

    PORTA = 0x10;      // Role bacagi haricindeki bacaklar sifir.
    PORTB = 0;
    TRISA = 0;         // Butun bacaklar cikti
    TRISB = 0xE0;      // ilk 3 bacak dugmelere bagli girdi
                       // geri kalan bacaklar cikti.

    guncelle();        // basamak degerlerini guncelle
    while(1)    {
        goster();      // degerleri goster
        klavye();      // dugmeleri oku
        role();
    }

}



/*    16F84A zamanlayici devre icin toplayici   
    19.06.2012    */

#include 
#define _XTAL_FREQ 4000000

__CONFIG(CP_OFF & WDTE_OFF & PWRTE_ON & FOSC_XT);

int sayi1 = 16;
int sayi2 =  9;
int toplam;
char basamak1, basamak2, basamak3, basamak4;

void interrupt isr()    {    // bos kesme hizmet programi
    #asm
        NOP
    #endasm
}

void guncelle()    {
    basamak1 = sayi1 % 10;        // en sagdaki basamak
    basamak2 = sayi1 / 10;
    basamak3 = sayi2 % 10;
    basamak4 = sayi2 / 10;        // en soldaki basamak
}

void goster()    {
    // basamaklari PortB'den gonder. PortA 7 segment'in basamaklarini tarar.
    PORTB = basamak1;
    PORTA = 0x01;
    __delay_ms(2);
   
    PORTB = basamak2;
    PORTA = 0x02;
    __delay_ms(2);

    PORTB = basamak3;
    PORTA = 0x04;
    __delay_ms(2);

    PORTB = basamak4;
    PORTA = 0x08;
    __delay_ms(2);

}

void klavye()    {
    if(RB7 == 0)    {
        toplam = sayi1 + sayi2;
        // ucuncu dugmeye basilinca iki sayiyi topla
        basamak4 = 0;
        basamak3 = toplam / 100;
        basamak2 = (toplam / 10) % 10;
        basamak1 = toplam % 10;
        // bu toplama icin kendi guncelleme rutini gerekli.
        while(RB7 == 0)    goster();
        // ucuncu dugme basili oldugu surece toplami ekranda goster
        guncelle();
        // sonra yine eski sayilarla ekrani guncelle
    }
   
    if(RB6 == 0)    {
        // ikinci dugme basiliysa birinci sayiyi arttir [0, 100]
        sayi1++;
        if(sayi1 == 100)    sayi1 = 0;
        guncelle();
        while(RB6 == 0)    goster();
    }

    if(RB5 == 0)    {
        // birinci dugme basiliysa ikinci sayiyi arttir [0, 100]
        sayi2++;
        if(sayi2 == 100)    sayi2 = 0;
        guncelle();
        while(RB5 == 0)    goster();
    }
}

void main()    {
   
    TMR0 = 0;        // Timer0 sifirlama
    GIE = 0;         // Kesmeleri durdur
    T0IE = 0;        // Zamanlayici kesmesini durur
    OPTION_REG = 0;   
    RBIF = 0;        // PortB kesmelerini durdur

    PORTA = 0x10;    // Roleye dokunma diger bacaklari sifirla
    PORTB = 0;
    TRISA = 0;       // PortA cikis.
    TRISB = 0xE0;    // PortB ilk 3 pin giris, digerleri cikis.

    guncelle();
    while(1)    {
        goster();
        klavye();
    }
}