3 puan yazan GN⁺ 2024-07-01 | 1 yorum | WhatsApp'ta paylaş
  • 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ı davranabilir
  • case etiketlerinin switch bloğunun en üst seviyesinde olması gerekmez; bu yüzden if (0) case... ile else if zincirini 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 (...) veya for (...) gibi süslü parantez olmadan da yazılabilir
    • switch (i) case 1: puts("i = 1"); derlenebilir
    • Süslü parantez olmadığında switch yalnızca tek bir deyime bağlanır; bu nedenle ardından gelen case 2: artık switch içinde olmadığı için hata olur
  • switch, özünde eşleşen case etiketine giden goto benzeri bir yapıdır
    • switch bloğu içinde int a = 123; ve puts(...) 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
  • case etiketlerinin, bağlı oldukları switch bloğunun en üst seviyesinde bulunması gerekmez
    • if (0) case 0: puts("i = 0"); gibi bir biçim de çalışır
    • switch, ilgili case etiketine doğrudan atladığı için önceki if (0) atlanır
    • puts(...) çalıştıktan sonra gelen diğer çıktı deyimleri ise hâlâ if (0) koşulu tarafından engellendiğinden, break olmadan da fallthrough önlenebilir
    • Örnek Godbolt üzerinde görülebilir
  • if ... else zinciri sözdizimsel olarak tek bir üst düzey deyim gibi davrandığından, bunu switch ile birleştirerek süslü parantezsiz tuhaf bir switch oluşturmak mümkündür
    • Örnek, case 1 ... 10 aralık durumu ve default durumunu da içerir
    • Örnek Godbolt üzerinde görülebilir
  • GNU uzantısı olan && işleci, bir etiketin adresini almayı ve o adrese goto yapmayı 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
  • Aynı GNU uzantısı kullanılarak for (...) olmadan da değişken bildirimi içinde etiket tabanlı bir döngü kurulabilir
    • Örnekte, i = 0 ile i = 5 arasını yazdıran bir döngü etiketler ve goto *&&_ 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
  • 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

 
GN⁺ 2024-07-01
Hacker News yorumları
  • Yukarıdaki örneğin a değ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.

    • C’de tanımsız davranış gerçekten zamanda geriye gitmez.
      Önceki komutları etkileyebilir, ancak kodun yeniden düzenlenmesi veya karmaşık dönüşümler tanımsız davranış olmasa da gerçekleşen şeylerdir.
    • C’de yalnızca ilklendirilmemiş otomatik saklama süreli nesnelere erişirken tanımsız davranış oluşur.
      Statik nesneler her zaman ilklendirildiği için böyle bir durum ortaya çıkmaz.
      Geriye malloc ile 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 break yazmak gerekmeyen bir switch gibi yapılabilir; #define brkcase if (0) case gibi 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;case de benzer çalışır gibi geliyor ama o zaman makronun amacı biraz ortadan kalkıyor.
    • Bu yöntemin nasıl çalıştığı çok belirsiz olduğundan, aynı kontrol akışını Duff's device tarzı switch suistimali yerine goto ile yazmak daha iyi olur.
    • Bu yöntem yalnızca case etiketinin gövdesi tek satırsa veya süslü parantezlerle sarılmışsa çalışır.
      Eskiden bu yapıyı “sonraki case etiketinin yalnızca ilk satırını atla, geri kalanı olduğu gibi fall-through olsun” anlamında kullanmıştım.
      case etiketlerini 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.

    • Yakın zamanda standartlaşmadıysa geçerli C değil, bir GNU uzantısıdır.
    • GNU C uzantısı gibi görünüyor: https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html
      Uzantının geçmişini bulamadım; bildiğim kadarıyla standart C’de yok. clang konusunda 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.

    • Küçük bir not: sys.stdout.write(f"{n}\n") yerine print(n) kullanılabilir.
      Şu anki kod, f-string dışında printin ifade olduğu Python 2 tarzı gibi görünüyor. Python 3’te print normal bir fonksiyondur ve None döndürdüğü için false olarak değerlendirilir; bu yüzden ilk and de or ile değiştirilmelidir.
  • Bir başka şaşırtıcı nokta da 4[arr] ifadesinin arr[4] ile aynı olması.

    • Dizi pointer’a dönüştüğü için temelde *(array_label+offset) olur; burada da *(offset+array_label) haline gelir.
      Yani *(arr+4) ile *(4+arr) aynı şeydir.
    • Aynı ilkeyle arr[i][j] ve j[i[arr]] de tam olarak aynıdır.
      Bilmeniz gereken tek şey a[x][y] ifadesinin (a[x])[y] ile aynı olduğu ve a[x] ifadesinin x[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.
    switch deyimiyle döngü de yapılabiliyor: https://twitter.com/lcamtuf/status/1807129116980007037

  • Bu tür switch numaraları Duff's deviceın önemli bir parçası değil mi diye düşünüyorum.

    • Duff's device, K&R C sözdiziminde switch bloğu ile döngünün karıştırılabilmesine, açıkça break edilmezse caselerin fall-through yapması seçimine ve C’de döngünün tekrar switchin 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 != 0 gibi can sıkıcı sınır durumlarını insanın değil aracın doğru ele almasını sağlıyor.