Tutorial

C Bukan Bahasa Tingkat Rendah? Argumen David Chisnall yang Membuka Mata

C Bukan Bahasa Tingkat Rendah? Argumen David Chisnall yang Membuka Mata

Kalau lo developer yang pernah ngoding C, pasti pernah dengar atau bahkan bilang sendiri: "C itu bahasa tingkat rendah." Argumennya klasik: C dekat dengan hardware, ngasih kontrol langsung ke memori, dan dipakai buat bikin kernel, driver, dan embedded system. Tapi David Chisnall, peneliti dari University of Cambridge yang berkutat di riset kompiler dan bahasa pemrograman, membalik anggapan itu dalam esai terkenalnya C Is Not a Low-Level Language, yang terbit di Communications of the ACM pada 2018. Judulnya provokatif, dan argumennya justru makin relevan di era pasca-Meltdown dan Spectre.

Esai ini ditulis di tengah hebohnya dua kerentanan CPU besar yang mengguncang industri pada awal 2018. Meltdown dan Spectre menunjukkan bahwa asumsi dasar tentang isolasi antara program dan hardware itu rapuh. Chisnall memakai momen itu untuk mengajukan pertanyaan yang lebih dalam: kalau C benar-benar bahasa tingkat rendah yang ngasih lo kontrol penuh atas mesin, kenapa optimisasi kompiler modern bisa menghasilkan perilaku yang jauh dari bayangan programmer? Kenapa model mental "C itu cuma lapisan tipis di atas assembly" ternyata menyesatkan?

Asumsi yang Salah: C Itu Lapisan Tipis di Atas Mesin

Argumen utama Chisnall berangkat dari model mental yang umum dipakai programmer C. Banyak yang membayangkan C sebagai bahasa yang hampir satu-ke-satu dengan assembly: setiap operasi di C punya padanan langsung di mesin, dan kalau lo nulis a = b + c, lo bisa bayangin instruksi ADD yang jalan di CPU. Model ini bikin programmer merasa punya kontrol penuh. Tapi kenyataannya, kompiler modern sudah jauh melampaui itu.

Kompiler C modern menjalankan optimisasi yang sangat agresif. Dia bisa mengubah urutan operasi, menghapus kode yang dianggap tidak berpengaruh, menggabungkan variabel, dan mengasumsikan hal-hal yang tidak tertulis di kode sumber. Semua ini sah menurut standar C, karena standar mendefinisikan perilaku program secara abstrak, bukan instruksi spesifik yang harus dihasilkan. Hasilnya, kode yang lo tulis bisa berubah drastis setelah dikompilasi, dan perubahan itu sering bikin programmer kaget.

Contoh paling sederhana adalah optimisasi yang memanfaatkan undefined behavior. Dalam C, banyak operasi yang dianggap undefined behavior, artinya kompiler bebas berasumsi operasi itu tidak akan pernah terjadi. Kalau program lo melanggar asumsi ini, kompiler bisa melakukan apa saja, termasuk menghapus kode yang lo anggap penting. Programmer yang menganggap C memberi kontrol penuh sering terjebak di sini: kontrol yang mereka rasakan sebenarnya adalah ilusi yang dijaga oleh disiplin ketat yang tidak selalu mereka sadari.

Kenapa Ini Berbeda dari Assembly

Supaya jelas, Chisnall tidak sedang bilang C sama jeleknya dengan bahasa tingkat tinggi lain. Dia bilang C itu bukan bahasa tingkat rendah dalam arti yang lo kira. Bahasa tingkat rendah yang sesungguhnya, seperti assembly, memberi lo akses langsung ke apa yang benar-benar terjadi di hardware. Lo tentukan register mana yang dipakai, lo tahu persis instruksi apa yang jalan, dan tidak ada kompiler yang diam-diam mengubah rencana lo.

C berbeda. C adalah bahasa portabel yang dirancang untuk bisa jalan di banyak arsitektur. Portabilitas ini dicapai dengan abstraksi: C tidak bicara soal register atau instruksi spesifik, tapi soal objek, tipe, dan operasi abstrak. Abstraksi ini yang bikin C bisa jalan di mana-mana, tapi juga yang bikin kompiler punya ruang gerak luas buat menerjemahkan kode lo. Ruang gerak itu yang dipakai untuk optimisasi, dan optimisasi itulah yang bikin perilaku C di level mesin sulit diprediksi.

Chisnall mencontohkan ini dengan membandingkan C dan arsitektur mesin modern. CPU modern bukan mesin yang sederhana. Ada pipeline, prediksi cabang, eksekusi spekulatif, dan cache bertingkat. Kompiler yang baik tahu semua ini dan menghasilkan kode yang memanfaatkannya. Kode yang dihasilkan sering tidak mirip sama sekali dengan struktur kode sumbernya. Jadi kalau lo membayangkan C sebagai cara ngomong langsung ke CPU, lo salah alamat: lo sebenarnya ngomong ke kompiler, dan kompiler yang ngomong ke CPU dengan caranya sendiri.

Konsekuensi untuk Keamanan

Bagian paling tajam dari esai Chisnall adalah analisis keamanannya. Kalau C benar-benar bahasa tingkat rendah yang kasih kontrol penuh, programmer seharusnya bisa bernalar dengan akurat tentang apa yang terjadi di memori dan mencegah bug keamanan. Tapi kenyataannya, kelas bug yang paling berbahaya di C, seperti buffer overflow dan use-after-free, justru sering muncul karena jurang antara model mental programmer dan perilaku aktual kompiler.

Meltdown dan Spectre memperparah masalah ini. Kedua kerentanan ini mengeksploitasi eksekusi spekulatif di CPU, sebuah optimisasi hardware yang jalan di bawah permukaan dan tidak terlihat oleh model mental programmer C. Tidak peduli seberapa hati-hati lo nulis C, lo tidak bisa bernalar tentang eksekusi spekulatif karena itu terjadi di level yang tidak terjangkau bahasa lo. Ini bukti bahwa kontrol yang dirasa programmer C punya batas yang tidak mereka sadari.

Chisnall berargumen bahwa keamanan butuh bahasa yang modelnya jujur tentang apa yang terjadi di mesin, atau bahasa yang membatasi programmer dengan cara yang membuat bug berbahaya mustahil terjadi. Bahasa seperti Rust muncul sebagai jawaban atas masalah ini: dia memberi kontrol rendah yang C janjikan, tapi dengan pemeriksaan yang membuat kelas bug tertentu tidak bisa dikompilasi. Ini bukan kebetulan; ini respons langsung terhadap kegagalan C sebagai bahasa yang bisa dipercaya untuk kode yang sensitif terhadap keamanan.

Kenapa C Masih Dipakai di Mana-Mana

Kalau C punya semua masalah ini, kenapa dia masih jadi tulang punggung industri? Jawabannya tidak sesederhana "programmer-nya males pindah". C punya ekosistem yang luar biasa: kernel Linux, sistem operasi, database, interpreter bahasa lain, dan hampir semua infrastruktur penting ditulis di C atau turunannya. Migrasi massal dari basis kode sebesar ini tidak bisa terjadi dalam semalam.

Selain itu, C menang dalam hal portabilitas dan ketersediaan kompiler. Hampir setiap arsitektur yang pernah ada punya kompiler C. Kalau lo nulis library di C, library itu bisa dipakai dari hampir semua bahasa lain lewat foreign function interface. Posisi C sebagai lingua franca sistem membuatnya sulit digulingkan, meskipun cacatnya makin dipahami.

Tapi arah industri sudah bergeser. Proyek baru yang butuh performa dan keamanan makin sering memilih Rust. Kernel Linux sudah menerima komponen Rust. Microsoft dan Google sudah menyatakan dukungan untuk Rust di komponen sistem yang sensitif. Ini bukan berarti C akan hilang, tapi berarti posisinya sebagai default untuk kode sistem mulai digantikan di area yang paling rawan.

Konteks untuk Developer Indonesia

Buat developer di Indonesia, esai Chisnall layak dibaca bukan cuma sebagai teori, tapi sebagai alat untuk mengambil keputusan teknis. Kalau lo baru mulai belajar bahasa sistem, pertanyaan "mulai dari C atau Rust?" jadi lebih mudah dijawab setelah paham argumen ini. C tetap berharga untuk dipelajari karena akar historisnya dan karena banyak kode yang harus lo baca tetap ditulis di C. Tapi kalau lo memulai proyek baru yang butuh keamanan dan kontrol, Rust menawarkan apa yang C janjikan tanpa banyak jebakan yang C sembunyikan.

Di sisi lain, pemahaman tentang gap antara C dan mesin juga berguna untuk debugging. Ketika kode C lo berperilaku aneh, alih-alih langsung menyalahkan hardware, coba lihat output assembly-nya. Disassemble hasil kompilasi, baca instruksi yang sebenarnya dihasilkan, dan bandingkan dengan apa yang lo bayangkan. Latihan ini sering membuka mata: kode yang lo tulis dan kode yang jalan bisa jadi dua hal yang sangat berbeda, dan memahami perbedaan itu adalah langkah pertama jadi programmer sistem yang matang.

Contoh Konkret: Optimisasi yang Mengubah Segalanya

Supaya argumen ini tidak terasa abstrak, ada baiknya lihat contoh konkret yang sering dibahas Chisnall dan peneliti lain. Anggap lo punya fungsi yang menulis ke sebuah pointer, lalu membacanya lagi. Dalam model mental programmer C, dua operasi ini berurutan dan tidak bisa diubah urutannya. Tapi kompiler bisa membuktikan bahwa tidak ada yang bisa mengubah nilai di antara dua operasi itu, lalu menghapus pembacaan kedua dan memakai nilai yang sudah ada di register. Hasilnya sama secara logika, tapi instruksi yang dihasilkan berbeda dari yang lo bayangkan.

Contoh lain yang lebih ekstrem melibatkan signed integer overflow. Dalam C, melimpahnya integer bertanda adalah undefined behavior. Kompiler memakai fakta ini untuk mengasumsikan bahwa operasi seperti x + 1 > x selalu benar, karena kalau overflow terjadi, programnya sudah dianggap rusak. Asumsi ini bikin kompiler bisa menghapus pemeriksaan yang programmer tulis dengan niat baik. Programmer yang tidak sadar aturan ini sering bingung kenapa kode mereka "hilang" setelah dikompilasi dengan optimisasi tinggi. Ini contoh sempurna dari jurang antara niat programmer dan perilaku kompiler.

Yang lebih menarik, kompiler modern juga bisa mengenali pola dan menggantinya dengan instruksi yang tidak ada padanannya di C. Misalnya, loop yang menjumlahkan array bisa dikenali kompiler dan diubah menjadi vektorisasi memakai instruksi SIMD, atau bahkan diganti dengan rumus matematika kalau polanya cocok. Programmer C yang menulis loop sederhana mungkin tidak pernah membayangkan hasil kompilasinya memakai instruksi yang baru ada di CPU generasi terbaru. Lagi-lagi, kode sumber bukanlah peta akurat dari kode mesin.

Apa Artinya untuk Pilihan Bahasa

Kalau lo menerima argumen Chisnall, konsekuensinya untuk pilihan bahasa cukup dalam. Bahasa tingkat rendah yang sesungguhnya, yang modelnya jujur tentang mesin, hampir tidak ada yang praktis dipakai untuk menulis aplikasi. Assembly memang jujur, tapi menulis aplikasi besar di assembly adalah mimpi buruk. Di sisi lain, bahasa yang aman dan produktif, seperti Rust, Go, atau bahkan bahasa managed, sering dianggap "jauh dari mesin". Padahal, kalau dipikir ulang, jarak ke mesin itu tidak selalu seburuk yang dibayangkan, karena C pun sebenarnya tidak sedekat yang selama ini diyakini.

Rust menarik karena dia mencoba mengambil posisi tengah yang sebelumnya diklaim C. Rust memberi kontrol atas alokasi memori, representasi data, dan perilaku low-level lain, tapi dengan tipe sistem yang bisa membuktikan keamanan di banyak kasus. Kompiler Rust juga melakukan optimisasi, tapi modelnya lebih jujur soal apa yang boleh dan tidak boleh dilakukan. Buat banyak pengamat, Rust adalah penerus spiritual C yang memperbaiki cacat fundamentalnya, bukan cuma kosmetik.

Pilihan bahasa tentu tidak bisa direduksi jadi satu esai. Faktor ekosistem, tim, dan kebutuhan proyek tetap dominan. Tapi esai Chisnall memberi kerangka berpikir yang berguna: sebelum memilih bahasa, tanyakan bukan cuma "seberapa dekat bahasa ini ke mesin?", tapi "seberapa jujur model bahasa ini tentang apa yang sebenarnya terjadi?". Kejujuran model itu, menurut Chisnall, lebih penting daripada kedekatan yang dirasa.

Kesimpulan

Judul esai Chisnall mungkin terdengar seperti clickbait, tapi argumennya serius dan relevan. C bukan bahasa tingkat rendah dalam arti memberi lo kontrol langsung ke mesin; C adalah bahasa portabel dengan abstraksi yang nyaman, yang perilaku sebenarnya di level mesin ditentukan oleh kompiler yang agresif. Ilusi kontrol ini punya konsekuensi nyata, terutama di keamanan, dan menjelaskan kenapa generasi bahasa baru seperti Rust muncul dan diterima.

Ini bukan vonis mati untuk C. C tetap penting, tetap dipakai, dan tetap layak dipelajari. Tapi cara lo memandang C harus berubah: bukan sebagai jendela bening ke hardware, tapi sebagai bahasa yang menuntut lo memahami dua lapis sekaligus, lapisan abstraksi C dan lapisan mesin yang sebenarnya, serta jurang di antara keduanya. Developer yang paham jurang ini akan menulis kode yang lebih baik, di bahasa apa pun.

💬 Komentar (0)

Belum ada komentar. Jadilah yang pertama! 💬

Komentar akan muncul setelah moderasi.