Proyek akhir pekan: tantangan seru dengan bahasa C
(lcamtuf.substack.com)- 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
casetidak harus hanya berada di tingkat teratas blokswitch, sehingga switch tanpa kurung kurawal yang menggabungkanif (0) case...dan rantaielse ifjuga 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, sepertiif (...)ataufor (...)switch (i) case 1: puts("i = 1");dapat dikompilasi- Tanpa kurung kurawal, hanya satu pernyataan yang menempel ke
switch, sehinggacase 2:berikutnya sudah tidak lagi berada di dalamswitchdan menjadi error
- Pada dasarnya,
switchadalah struktur yang lebih mirip goto yang berpindah ke label case yang cocok- Walaupun di dalam blok
switchadaint a = 123;danputs(...), jika eksekusi melompat kedefault:, kode inisialisasi sebelumnya tidak dijalankan - Dalam kasus ini,
atidak diinisialisasi menjadi 123, dan secara teknis itu termasuk undefined behavior - Contohnya bisa dilihat di Godbolt
- Walaupun di dalam blok
- Label
casetidak harus hanya berada di tingkat teratas blokswitchyang terhubung- Bentuk seperti
if (0) case 0: puts("i = 0");juga tetap bekerja switchakan melompat langsung kecasetersebut, sehinggaif (0)sebelumnya dilewati- Setelah
puts(...)dijalankan, pernyataan output lain sesudahnya tetap terhalang oleh kondisiif (0), sehingga fallthrough bisa dihindari bahkan tanpabreak - Contohnya bisa dilihat di Godbolt
- Bentuk seperti
- Rantai
if ... elsesecara sintaks bertindak seperti satu pernyataan tingkat teratas, sehingga bisa digabung denganswitchuntuk membuat switch aneh tanpa kurung kurawal- Contohnya mencakup range case
case 1 ... 10dan jugadefault - Contohnya bisa dilihat di Godbolt
- Contohnya mencakup range case
- Operator
&&, yang merupakan ekstensi GNU, memungkinkan kita mengambil alamat suatu label lalu melakukangotoke 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 ini, kita bisa mengimplementasikan switch buatan sendiri dengan cara seperti
- 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 = 0sampaii = 5menggunakan label dangoto *&&_ - Contohnya bisa dilihat di Godbolt
- Snippet terakhir ini khusus GCC, dan mungkin tidak aman terhadap undefined behavior
- Contohnya menyusun loop yang mencetak dari
- Di C, bahkan tanpa sengaja melakukan obfuscation, kita tetap bisa membuat kode yang sepenuhnya terasa asing dan membingungkan
1 komentar
Komentar Hacker News
Contoh di atas katanya mencetak nilai
a, tetapi tidak diinisialisasi menjadi 123; namun di C, hal itu memang bisa saja terjadiMenggunakan 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,
returnyang menyusul atau bahkan instruksi sebelumnya pun bisa dihapus lewat optimisasi, sehingga perilaku tak terdefinisi bisa tampak seolah-olah berdampak “mundur ke masa lalu”Ia bisa memengaruhi instruksi sebelumnya, tetapi penyusunan ulang kode atau transformasi kompleks juga bisa terjadi meski tanpa perilaku tak terdefinisi
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 gagalSintaks 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
switchyang tidak perlu menulisbreakdi akhir tiap cabang, dan makro seperti#define brkcase if (0) casejuga memungkinkan. Kompiler mungkin tidak menyukai alur kontrolnya, tetapi kemungkinan besar akan bisa menghapusnya dengan baik#define brkcase break;casejuga tampaknya bekerja mirip, tetapi kalau begitu tujuan makronya agak hilanggotocaseberupa satu baris atau dibungkus dengan kurung kurawalDulu saya pernah memakai struktur ini dengan makna “lewati hanya baris pertama dari label
caseberikutnya, lalu sisanya tetap fallthrough”Jika label
casedipandang bukan sebagai pemisah antarpernyataan, melainkan sekadar label biasa, semuanya jadi masuk akalTeknik 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 validSaya sudah memakai C bertahun-tahun, jadi saya penasaran ini berasal dari standar yang mana
Saya tidak menemukan sejarah ekstensi ini, dan setahu saya ini tidak ada di C standar. Untuk
clang, saya tidak yakinDulu 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 rekursifsys.stdout.write(f"{n}\n")bisa diganti denganprint(n)Kode sekarang terlihat seperti gaya Python 2 saat
printmasih berupa statement, kecuali penggunaanf-string. Di Python 3,printadalah fungsi biasa dan mengembalikanNoneyang bernilai false, jadiandpertama juga harus diganti menjadiorHal mengejutkan lainnya adalah bahwa
4[arr]sama denganarr[4]*(array_label+offset), dan di sini menjadi*(offset+array_label)Artinya
*(arr+4)dan*(4+arr)adalah hal yang samaarr[i][j]danj[i[arr]]juga persis samaCukup pahami bahwa
a[x][y]sama dengan(a[x])[y], dana[x]sama denganx[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/1807129116980007037Saya rasa trik
switchseperti ini adalah bagian penting dari Duff's deviceswitchbisa dicampur dengan loop, bahwacaseakan fallthrough jika tidak di-breaksecara eksplisit, dan bahwa loop di C bisa melompat kembali ke bagian dalamswitchDuff 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 != 0ditangani dengan benar oleh alat, bukan oleh manusia