3 poin oleh GN⁺ 2024-07-01 | 1 komentar | Bagikan ke WhatsApp
  • C itu sederhana dan ekspresif, tetapi hanya dengan pernyataan switch dan aturan label saja, kita bisa membuat kode yang bahkan membingungkan pengembang berpengalaman
  • Obfuscation ala IOCCC menyembunyikan kode dengan makro dan formatting, tetapi hanya dengan sintaks C itu sendiri pun kode yang bisa dibaca tetapi terasa asing tetap mungkin dibuat
  • switch (...) tidak mewajibkan kurung kurawal, dan karena ia melompat ke label case yang cocok, perilakunya bisa berbeda dari alur inisialisasi blok yang umum
  • Label case tidak harus hanya berada di tingkat teratas blok switch, sehingga switch tanpa kurung kurawal yang menggabungkan if (0) case... dan rantai else if juga bisa dikompilasi
  • Ekstensi alamat label && di GNU C bahkan memungkinkan switch buatan sendiri atau loop berbasis label, tetapi beberapa contohnya khusus GCC dan mungkin tidak aman terhadap undefined behavior

Kode asing yang lahir dari sintaks C

  • C punya banyak kekurangan, tetapi karena sintaksnya sederhana dan ekspresif, bahasa ini dipakai untuk menulis perangkat lunak besar seperti sistem operasi
  • Sintaksnya yang ringkas juga memengaruhi struktur kode banyak bahasa penerus arus utama, dari Java hingga Go
  • Contoh paling terkenal dari obfuscation kode adalah IOCCC
    • Karya pemenang IOCCC sering tersusun dari makro preprocessor, formatting yang tidak lazim, nama variabel yang tidak membantu, dan ekspresi aritmetika yang sulit dipahami
    • Kode seperti ini memang mengagumkan, tetapi karena harus direkayasa balik menjadi kode normal, rasanya bisa kurang menyenangkan untuk dipelajari

Sudut terdalam C yang terlihat lewat switch, case, dan goto

  • switch (...) bisa ditulis tanpa kurung kurawal, seperti if (...) atau for (...)
    • switch (i) case 1: puts("i = 1"); dapat dikompilasi
    • Tanpa kurung kurawal, hanya satu pernyataan yang menempel ke switch, sehingga case 2: berikutnya sudah tidak lagi berada di dalam switch dan menjadi error
  • Pada dasarnya, switch adalah struktur yang lebih mirip goto yang berpindah ke label case yang cocok
    • Walaupun di dalam blok switch ada int a = 123; dan puts(...), jika eksekusi melompat ke default:, kode inisialisasi sebelumnya tidak dijalankan
    • Dalam kasus ini, a tidak diinisialisasi menjadi 123, dan secara teknis itu termasuk undefined behavior
    • Contohnya bisa dilihat di Godbolt
  • Label case tidak harus hanya berada di tingkat teratas blok switch yang terhubung
    • Bentuk seperti if (0) case 0: puts("i = 0"); juga tetap bekerja
    • switch akan melompat langsung ke case tersebut, sehingga if (0) sebelumnya dilewati
    • Setelah puts(...) dijalankan, pernyataan output lain sesudahnya tetap terhalang oleh kondisi if (0), sehingga fallthrough bisa dihindari bahkan tanpa break
    • Contohnya bisa dilihat di Godbolt
  • Rantai if ... else secara sintaks bertindak seperti satu pernyataan tingkat teratas, sehingga bisa digabung dengan switch untuk membuat switch aneh tanpa kurung kurawal
    • Contohnya mencakup range case case 1 ... 10 dan juga default
    • Contohnya bisa dilihat di Godbolt
  • Operator &&, yang merupakan ekstensi GNU, memungkinkan kita mengambil alamat suatu label lalu melakukan goto ke alamat itu
    • Dengan ini, kita bisa mengimplementasikan switch buatan sendiri dengan cara seperti goto *(void*[]){ &&case_0, &&case_1, &&case_2 }[i];
    • Contohnya bisa dilihat di Godbolt
  • Dengan ekstensi GNU yang sama, kita juga bisa membuat loop berbasis label di dalam deklarasi variabel tanpa for (...)
    • Contohnya menyusun loop yang mencetak dari i = 0 sampai i = 5 menggunakan label dan goto *&&_
    • Contohnya bisa dilihat di Godbolt
    • Snippet terakhir ini khusus GCC, dan mungkin tidak aman terhadap undefined behavior
  • Di C, bahkan tanpa sengaja melakukan obfuscation, kita tetap bisa membuat kode yang sepenuhnya terasa asing dan membingungkan

1 komentar

 
GN⁺ 2024-07-01
Komentar Hacker News
  • Contoh di atas katanya mencetak nilai a, tetapi tidak diinisialisasi menjadi 123; namun di C, hal itu memang bisa saja terjadi
    Menggunakan variabel yang belum diinisialisasi bukan berarti membaca “nilai yang kebetulan masih tersisa di memori itu”, melainkan perilaku tak terdefinisi, jadi kompiler boleh melakukan apa pun
    Misalnya, memori tersebut bisa saja selalu diinisialisasi ke 123, atau seluruh potongan itu dianggap sebagai perilaku tak terdefinisi lalu semua instruksi dihapus sehingga tidak mencetak apa pun. Lebih jauh lagi, return yang menyusul atau bahkan instruksi sebelumnya pun bisa dihapus lewat optimisasi, sehingga perilaku tak terdefinisi bisa tampak seolah-olah berdampak “mundur ke masa lalu”

    • Di C, perilaku tak terdefinisi sebenarnya tidak benar-benar berjalan mundur ke masa lalu
      Ia bisa memengaruhi instruksi sebelumnya, tetapi penyusunan ulang kode atau transformasi kompleks juga bisa terjadi meski tanpa perilaku tak terdefinisi
    • Ini hanya menjadi perilaku tak terdefinisi saat mengakses objek dengan automatic storage duration yang belum diinisialisasi di C
      Objek statis selalu diinisialisasi, jadi situasi seperti ini tidak muncul
      Yang tersisa adalah objek dinamis seperti anggota struct yang belum diinisialisasi dari hasil alokasi malloc; membaca memori dinamis yang belum diinisialisasi bukanlah perilaku tak terdefinisi di C, dan Anda akan mendapatkan nilai yang direpresentasikan oleh bit yang belum diinisialisasi itu. Jika tipe tersebut tidak memiliki trap representation, ini tidak akan gagal
  • Sintaks seperti ini juga memungkinkan
    switch(k) { if (0) case 0: x = 1; if (0) case 1: x = 2; if (0) default: x = 3; }
    Ini bisa dibuat seperti switch yang tidak perlu menulis break di akhir tiap cabang, dan makro seperti #define brkcase if (0) case juga memungkinkan. Kompiler mungkin tidak menyukai alur kontrolnya, tetapi kemungkinan besar akan bisa menghapusnya dengan baik

    • #define brkcase break;case juga tampaknya bekerja mirip, tetapi kalau begitu tujuan makronya agak hilang
    • Cara kerja pendekatan ini sangat tidak jelas, jadi daripada penyalahgunaan switch ala Duff's device, lebih baik menulis alur kontrol yang sama dengan goto
    • Pendekatan ini hanya bekerja ketika isi label case berupa satu baris atau dibungkus dengan kurung kurawal
      Dulu saya pernah memakai struktur ini dengan makna “lewati hanya baris pertama dari label case berikutnya, lalu sisanya tetap fallthrough”
      Jika label case dipandang bukan sebagai pemisah antarpernyataan, melainkan sekadar label biasa, semuanya jadi masuk akal
  • Teknik ini bisa dipakai untuk mengimplementasikan coroutine di C: https://stackoverflow.com/questions/24202890/switch-based-co...

  • Saya tidak tahu kenapa saya tidak sadar bahwa case 1 ... 10: adalah C yang valid
    Saya sudah memakai C bertahun-tahun, jadi saya penasaran ini berasal dari standar yang mana

    • Kalau ini tidak baru-baru ini distandardisasi, maka ini bukan C yang valid melainkan ekstensi GNU
    • Sepertinya ini ekstensi GNU C: https://gcc.gnu.org/onlinedocs/gcc/Case-Ranges.html
      Saya tidak menemukan sejarah ekstensi ini, dan setahu saya ini tidak ada di C standar. Untuk clang, saya tidak yakin
  • Dulu saya pernah iseng menulis kode C aneh yang menghitung mundur dari 10 ke 1
    Versi C, Python, dan shell semuanya bisa dibuat jadi satu baris dengan memakai && dan pemanggilan rekursif

    • Sedikit catatan, sys.stdout.write(f"{n}\n") bisa diganti dengan print(n)
      Kode sekarang terlihat seperti gaya Python 2 saat print masih berupa statement, kecuali penggunaan f-string. Di Python 3, print adalah fungsi biasa dan mengembalikan None yang bernilai false, jadi and pertama juga harus diganti menjadi or
  • Hal mengejutkan lainnya adalah bahwa 4[arr] sama dengan arr[4]

    • Karena array terkonversi menjadi pointer, pada dasarnya ini adalah *(array_label+offset), dan di sini menjadi *(offset+array_label)
      Artinya *(arr+4) dan *(4+arr) adalah hal yang sama
    • Dengan prinsip yang sama, arr[i][j] dan j[i[arr]] juga persis sama
      Cukup pahami bahwa a[x][y] sama dengan (a[x])[y], dan a[x] sama dengan x[a]
      arr[i][j](arr[i])[j](i[arr])[j]j[i[arr]]
  • Potongan kode obfuscated di akhir tulisan juga menunjukkan ekstensi GCC lainnya: https://stackoverflow.com/questions/34559705/ternary-conditi...

  • Saya pertama kali melihat teknik-teknik usil penulis blog ini di Twitter
    Dengan pernyataan switch, Anda juga bisa membuat loop: https://twitter.com/lcamtuf/status/1807129116980007037

  • Saya rasa trik switch seperti ini adalah bagian penting dari Duff's device

    • Duff's device bergantung pada fakta bahwa dalam sintaks C K&R, blok switch bisa dicampur dengan loop, bahwa case akan fallthrough jika tidak di-break secara eksplisit, dan bahwa loop di C bisa melompat kembali ke bagian dalam switch
      Duff sedang mencoba mengoptimalkan memory-mapped I/O (MMIO), dan di C masa kini orang mungkin tidak akan melakukannya dengan cara seperti ini. Sekarang MMIO tidak lagi secepat instruksi CPU, dan jika datanya sedikit lebih banyak pun kita bisa memakai DMA
      Dalam bahasa modern, MMIO juga tidak akan diperlakukan sebagai sekadar pointer dereference biasa, tetapi di C hal itu terus dipertahankan dengan menambahkan jalan memutar ke sistem tipenya
      Menurut saya pribadi, penerus “Device” milik Tom Duff adalah mekanisme iterate loops di WUFFS. Mekanisme ini menentukan cara me-unroll sebagian N langkah dari sebuah loop, sambil menjanjikan hasil yang sama seperti menjalankan tubuh loop utama sebanyak N kali, tetapi bisa lebih cepat. Vektorisasi jadi lebih mudah memahami maksudnya, dan kasus batas yang merepotkan seperti M % N != 0 ditangani dengan benar oleh alat, bukan oleh manusia