Hafta Sonu Projesi: C Diliyle Eğlenceli Bir Meydan Okuma
(lcamtuf.substack.com)- C basit ve ifade gücü yüksek bir dil olsa da, yalnızca switch deyimi ve etiket kurallarıyla bile deneyimli geliştiricileri şaşırtabilecek kodlar yazmak mümkündür
- IOCCC tarzı obfuscation, makrolar ve biçimlendirmeyle kodu gizler; ancak yalnızca C’nin kendi sözdizimiyle de okunabilir ama yabancı gelen kod üretmek mümkündür
switch (...)için süslü parantez zorunlu değildir ve eşleşen case etiketine atladığı için, normal blok başlatma akışından farklı davranabilircaseetiketlerininswitchbloğunun en üst seviyesinde olması gerekmez; bu yüzdenif (0) case...ileelse ifzincirini birleştiren süslü parantezsiz switch yapıları da derlenir- GNU C’nin
&&etiket adresi genişlemesi, elle yazılmış switch yapılarından etiket tabanlı döngülere kadar imkân tanır; ancak bazı örnekler yalnızca GCC’ye özeldir ve tanımsız davranışa karşı güvenli olmayabilir
C sözdiziminin ürettiği tuhaf kod
- C’nin birçok kusuru vardır, ancak basit sözdizimi ve ifade gücü sayesinde işletim sistemi gibi büyük yazılımların yazılabildiği bir dil olarak kullanılır
- Kısa ve öz sözdizimi, Java’dan Go’ya kadar birçok ana akım ardıl dilin kod yapısını da etkilemiştir
- Kod obfuscation denince akla gelen en bilinen örneklerden biri IOCCC'dir
- IOCCC kazananları çoğu zaman önişlemci makroları, alışılmadık biçimlendirme, işe yaramayan değişken adları ve anlaşılması zor aritmetik ifadeler içerir
- Bu tür kodlar hayranlık uyandırabilir, ancak normal koda dönüştürülerek tersine mühendislik yapılması gerektiğinden öğrenme açısından daha az eğlenceli olabilir
switch, case ve goto ile ortaya çıkan C’nin derin köşeleri
switch (...),if (...)veyafor (...)gibi süslü parantez olmadan da yazılabilirswitch (i) case 1: puts("i = 1");derlenebilir- Süslü parantez olmadığında
switchyalnızca tek bir deyime bağlanır; bu nedenle ardından gelencase 2:artıkswitchiçinde olmadığı için hata olur
switch, özünde eşleşencaseetiketine giden goto benzeri bir yapıdırswitchbloğu içindeint a = 123;veputs(...)bulunsa bile,default:etiketine atlanırsa ondan önceki başlatma kodu çalışmaz- Bu durumda
a, 123 ile başlatılmış olmaz ve teknik olarak tanımsız davranış ortaya çıkar - Örnek Godbolt üzerinde görülebilir
caseetiketlerinin, bağlı olduklarıswitchbloğunun en üst seviyesinde bulunması gerekmezif (0) case 0: puts("i = 0");gibi bir biçim de çalışırswitch, ilgilicaseetiketine doğrudan atladığı için öncekiif (0)atlanırputs(...)çalıştıktan sonra gelen diğer çıktı deyimleri ise hâlâif (0)koşulu tarafından engellendiğinden,breakolmadan da fallthrough önlenebilir- Örnek Godbolt üzerinde görülebilir
if ... elsezinciri sözdizimsel olarak tek bir üst düzey deyim gibi davrandığından, bunuswitchile birleştirerek süslü parantezsiz tuhaf bir switch oluşturmak mümkündür- Örnek,
case 1 ... 10aralık durumu vedefaultdurumunu da içerir - Örnek Godbolt üzerinde görülebilir
- Örnek,
- GNU uzantısı olan
&&işleci, bir etiketin adresini almayı ve o adresegotoyapmayı mümkün kılar- Böylece
goto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i];gibi bir yöntemle elle yazılmış bir switch uygulanabilir - Örnek Godbolt üzerinde görülebilir
- Böylece
- Aynı GNU uzantısı kullanılarak
for (...)olmadan da değişken bildirimi içinde etiket tabanlı bir döngü kurulabilir- Örnekte,
i = 0ilei = 5arasını yazdıran bir döngü etiketler vegoto *&&_ile kurulmuştur - Örnek Godbolt üzerinde görülebilir
- Bu son snippet yalnızca GCC’ye özeldir ve tanımsız davranış açısından güvenli olmayabilir
- Örnekte,
- C’de kod özellikle obfuscation amacıyla yazılmasa bile tamamen yabancı ve kafa karıştırıcı biçimler oluşturmak mümkündür
1 yorum
Hacker News yorumları
Yukarıdaki örneğin
adeğerini yazdırdığını ama 123 ile ilklendirilmediğini söyledi; ancak C’de gerçekten böyle de olabilir.İlklendirilmemiş bir değişkeni kullanmak, “o bellekte tesadüfen kalmış değeri” okumak anlamına gelmez; tanımsız davranış olduğu için derleyici istediğini yapabilir.
Örneğin ilgili belleği koşulsuz olarak 123 ile ilklendirebilir ya da tüm parçayı tanımsız davranış sayıp komutların hepsini kaldırarak hiçbir şey yazdırmamasını sağlayabilir. Hatta ardından gelen
returnü ya da önceki komutları bile optimizasyonla kaldırabilir; bu yüzden tanımsız davranış “zamanda geriye gidip” etki ediyormuş gibi görünebilir.Önceki komutları etkileyebilir, ancak kodun yeniden düzenlenmesi veya karmaşık dönüşümler tanımsız davranış olmasa da gerçekleşen şeylerdir.
Statik nesneler her zaman ilklendirildiği için böyle bir durum ortaya çıkmaz.
Geriye
mallocile ayrılmış bir yapının ilklendirilmemiş üyeleri gibi dinamik nesneler kalır; ilklendirilmemiş dinamik belleği okumak C’de tanımsız davranış değildir ve ilklendirilmemiş bitlerin ifade ettiği değeri elde edersiniz. İlgili tipte tuzak gösterim yoksa başarısız olamaz.Şu tür bir sözdizimi de mümkün:
switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }Her dalın sonunda
breakyazmak gerekmeyen birswitchgibi yapılabilir;#define brkcase if (0) casegibi bir makro da mümkün. Derleyici kontrol akışını pek sevmeyecektir ama büyük olasılıkla çoğunu güzelce silip atar.#define brkcase break;casede benzer çalışır gibi geliyor ama o zaman makronun amacı biraz ortadan kalkıyor.gotoile yazmak daha iyi olur.caseetiketinin gövdesi tek satırsa veya süslü parantezlerle sarılmışsa çalışır.Eskiden bu yapıyı “sonraki
caseetiketinin yalnızca ilk satırını atla, geri kalanı olduğu gibi fall-through olsun” anlamında kullanmıştım.caseetiketlerini ifadeler arasındaki ayraçlar değil, basit etiketler olarak görürseniz hepsi anlamlı oluyor.C’de coroutine uygulamak için bu teknik kullanılabilir: https://stackoverflow.com/questions/24202890/switch-based-co...
case 1 ... 10:ifadesinin geçerli C olduğunu neden bilmediğimi bilmiyorum.C’yi yıllarca kullandım; bunun hangi standarttan geldiğini merak ediyorum.
Uzantının geçmişini bulamadım; bildiğim kadarıyla standart C’de yok.
clangkonusunda emin değilim.Eskiden eğlencesine 10’dan 1’e geri sayan tuhaf bir C kodu yazmıştım.
C, Python ve shell sürümlerinin hepsi
&&ve özyinelemeli çağrı kullanılarak tek satırlık yapılabiliyor.sys.stdout.write(f"{n}\n")yerineprint(n)kullanılabilir.Şu anki kod,
f-stringdışındaprintin ifade olduğu Python 2 tarzı gibi görünüyor. Python 3’teprintnormal bir fonksiyondur veNonedöndürdüğü için false olarak değerlendirilir; bu yüzden ilkanddeorile değiştirilmelidir.Bir başka şaşırtıcı nokta da
4[arr]ifadesininarr[4]ile aynı olması.*(array_label+offset)olur; burada da*(offset+array_label)haline gelir.Yani
*(arr+4)ile*(4+arr)aynı şeydir.arr[i][j]vej[i[arr]]de tam olarak aynıdır.Bilmeniz gereken tek şey
a[x][y]ifadesinin(a[x])[y]ile aynı olduğu vea[x]ifadesininx[a]ile aynı olduğudur.arr[i][j]→(arr[i])[j]→(i[arr])[j]→j[i[arr]]Yazının sonundaki obfuscation kod parçası başka bir GCC uzantısını gösteriyor: https://stackoverflow.com/questions/34559705/ternary-conditi...
Bu blog yazarının bu tür oyunbaz tekniklerini önce Twitter’da görmüştüm.
switchdeyimiyle döngü de yapılabiliyor: https://twitter.com/lcamtuf/status/1807129116980007037Bu tür
switchnumaraları Duff's deviceın önemli bir parçası değil mi diye düşünüyorum.switchbloğu ile döngünün karıştırılabilmesine, açıkçabreakedilmezsecaselerin fall-through yapması seçimine ve C’de döngünün tekrarswitchin içine atlayabilmesine dayanır.Duff’un amacı bellek eşlemeli G/Ç’yi (MMIO) optimize etmekti; bugün C’de bile bunu bu şekilde yapmazdık. Artık MMIO, CPU komut hızıyla kıyaslanacak kadar hızlı değil ve veri biraz fazla olduğunda DMA kullanılabiliyor.
Modern bir dil MMIO’yu basit bir pointer dolaylı başvurusu olarak da ele almazdı; C’de ise bunu sürdürebilmek için tip sistemine sürekli kaçış mekanizmaları eklendi.
Kişisel olarak Tom Duff’ın “Device”ının devamı olarak WUFFS’un iterate loops mekanizmasını görüyorum. Döngünün N adımını kısmen açmanın yolunu belirtirken, ana döngü gövdesini N kez çalıştırmakla aynı sonucu verdiğini ama daha hızlı olabileceğini vaat eden bir yaklaşım. Vektörleştirme niyeti daha kolay anlıyor ve
M % N != 0gibi can sıkıcı sınır durumlarını insanın değil aracın doğru ele almasını sağlıyor.