4 kesalahan umum pemrograman C - dan 5 tip untuk menghindarinya

Beberapa bahasa pemrograman dapat menyamai C hanya karena kecepatan dan kekuatan tingkat mesin. Pernyataan ini benar 50 tahun yang lalu, dan itu masih berlaku sampai sekarang. Namun, ada alasan programmer menciptakan istilah "footgun" untuk menggambarkan jenis kekuatan C. Jika Anda tidak berhati-hati, C dapat meledakkan jari kaki Anda — atau jari kaki orang lain.

Berikut adalah empat kesalahan paling umum yang dapat Anda lakukan dengan C, dan lima langkah yang dapat Anda lakukan untuk mencegahnya.

Kesalahan umum C: Tidak membebaskan mallocmemori (atau mengosongkannya lebih dari sekali)

Ini adalah salah satu kesalahan besar dalam C, banyak di antaranya melibatkan manajemen memori. Memori yang dialokasikan (selesai menggunakan malloc fungsi) tidak otomatis dibuang ke C. Ini tugas programmer untuk membuang memori itu saat tidak lagi digunakan. Gagal membebaskan permintaan memori berulang, dan Anda akan berakhir dengan kebocoran memori. Coba gunakan wilayah memori yang sudah dibebaskan, dan program Anda akan macet — atau, lebih buruk lagi, akan pincang dan menjadi rentan terhadap serangan menggunakan mekanisme itu.

Perhatikan bahwa kebocoran memori seharusnya hanya menjelaskan situasi di mana memori seharusnya dibebaskan, tetapi sebenarnya tidak. Jika sebuah program terus mengalokasikan memori karena memori sebenarnya dibutuhkan dan digunakan untuk bekerja, maka penggunaan memorinya mungkin tidak  efisien , tetapi sebenarnya itu bukan kebocoran.

Kesalahan umum C: Membaca larik di luar batas

Di sini kita memiliki kesalahan lain yang paling umum dan berbahaya di C. Pembacaan setelah akhir larik dapat mengembalikan data sampah. Tulisan melewati batas-batas array dapat merusak status program, atau merusaknya sepenuhnya, atau, yang terburuk, menjadi vektor serangan untuk malware.

Jadi mengapa beban pemeriksaan batas array diserahkan kepada programmer? Dalam spesifikasi C resmi, membaca atau menulis larik di luar batasnya adalah "perilaku tidak terdefinisi", yang berarti spesifikasi tersebut tidak menentukan apa yang seharusnya terjadi. Kompilator bahkan tidak perlu mengeluh tentang itu.

C telah lama lebih menyukai memberikan kekuatan kepada programmer bahkan dengan risiko mereka sendiri. Pembacaan atau penulisan di luar batas biasanya tidak terjebak oleh kompilator, kecuali Anda secara khusus mengaktifkan opsi kompilator untuk menjaganya. Terlebih lagi, sangat mungkin untuk melebihi batas dari sebuah array pada waktu proses dengan cara yang bahkan pemeriksaan compiler tidak dapat melindungi.

Kesalahan C umum: Tidak memeriksa hasil malloc

malloc dan calloc (untuk pre-zeroed memory) adalah fungsi pustaka C yang memperoleh memori yang dialokasikan heap dari sistem. Jika mereka tidak dapat mengalokasikan memori, mereka menghasilkan kesalahan. Kembali pada hari-hari ketika komputer memiliki memori yang relatif kecil, ada kemungkinan panggilan ke mallocmungkin tidak berhasil.

Meskipun komputer saat ini memiliki gigabyte RAM untuk digunakan, masih selalu ada kemungkinan mallocgagal, terutama di bawah tekanan memori tinggi atau ketika mengalokasikan lempengan besar memori sekaligus. Hal ini terutama berlaku untuk program C yang "mengalokasikan slab" blok besar memori dari OS terlebih dahulu dan kemudian membaginya untuk digunakan sendiri. Jika alokasi pertama gagal karena terlalu besar, Anda mungkin dapat menjebak penolakan tersebut, menurunkan alokasi, dan menyesuaikan heuristik penggunaan memori program. Tetapi jika alokasi memori gagal tidak terikat, seluruh program dapat mengalami gangguan.

Kesalahan umum C: Menggunakan void*pointer umum ke memori

Menggunakan  void* untuk menunjuk pada ingatan adalah kebiasaan lama — dan kebiasaan buruk. Pointer ke memori harus selalu char*, unsigned char*, atau  uintptr_t*. Rangkaian kompilator C modern harus disediakan uintptr_tsebagai bagian dari stdint.h

Saat diberi label dengan salah satu cara ini, jelas bahwa penunjuk merujuk ke lokasi memori dalam abstrak, bukan ke beberapa tipe objek yang tidak ditentukan. Ini sangat penting jika Anda melakukan matematika penunjuk. Dengan  uintptr_t*dan sejenisnya, elemen ukuran yang ditunjukkan, dan bagaimana elemen itu akan digunakan, tidak ambigu. Dengan void*, tidak terlalu banyak.

Menghindari kesalahan C umum - 5 tips

Bagaimana Anda menghindari kesalahan yang terlalu umum ini saat bekerja dengan memori, array, dan pointer di C? Ingatlah lima tip ini. 

Buat struktur program C sehingga kepemilikan memori tetap jelas

Jika Anda baru memulai aplikasi C, ada baiknya memikirkan cara memori dialokasikan dan dilepaskan sebagai salah satu prinsip organisasi untuk program tersebut. Jika tidak jelas di mana alokasi memori yang diberikan dibebaskan atau dalam keadaan apa, Anda mencari masalah. Lakukan upaya ekstra untuk membuat kepemilikan memori sejelas mungkin. Anda akan membantu diri Anda sendiri (dan pengembang masa depan).

Ini adalah filosofi di balik bahasa seperti Rust. Rust tidak memungkinkan untuk menulis program yang dapat dikompilasi dengan benar kecuali Anda secara jelas mengungkapkan bagaimana memori dimiliki dan ditransfer. C tidak memiliki batasan seperti itu, tetapi bijaksana untuk mengadopsi filosofi itu sebagai cahaya penuntun bila memungkinkan.

Gunakan opsi compiler C yang melindungi dari masalah memori

Banyak masalah yang dijelaskan di paruh pertama artikel ini dapat ditandai dengan menggunakan opsi compiler yang ketat. Edisi terbaru gcc, misalnya, menyediakan alat seperti AddressSanitizer ("ASAN") sebagai opsi kompilasi untuk memeriksa kesalahan umum manajemen memori.

Berhati-hatilah, alat ini tidak benar-benar menangkap semuanya. Mereka adalah pagar pembatas; mereka tidak memegang kemudi jika Anda melakukan off-road. Selain itu, beberapa fitur ini, seperti ASAN, mengenakan biaya kompilasi dan waktu proses, sehingga harus dihindari dalam build rilis.

Gunakan Cppcheck atau Valgrind untuk menganalisis kode C untuk kebocoran memori

Jika kompilernya sendiri gagal, alat lain akan masuk untuk mengisi kekosongan tersebut — terutama dalam hal menganalisis perilaku program pada waktu proses.

Cppcheck menjalankan analisis statis pada kode sumber C untuk mencari kesalahan umum dalam manajemen memori dan perilaku tidak terdefinisi (antara lain).

Valgrind menyediakan alat cache untuk mendeteksi memori dan kesalahan utas dalam menjalankan program C. Ini jauh lebih efektif daripada menggunakan analisis waktu kompilasi, karena Anda dapat memperoleh informasi tentang perilaku program ketika program itu benar-benar tayang. Sisi negatifnya adalah program berjalan pada sebagian kecil dari kecepatan normalnya. Tapi ini umumnya bagus untuk pengujian.

Alat-alat ini bukanlah peluru perak dan mereka tidak akan menangkap semuanya. Tapi mereka bekerja sebagai bagian dari strategi pertahanan umum melawan kesalahan manajemen memori di C.

Otomatiskan manajemen memori C dengan pengumpul sampah

Karena kesalahan memori adalah sumber masalah C yang mencolok, berikut satu solusi mudah: Jangan kelola memori di C secara manual. Gunakan pengumpul sampah. 

Ya, ini dimungkinkan di C. Anda dapat menggunakan sesuatu seperti pengumpul sampah Boehm-Demers-Weiser untuk menambahkan manajemen memori otomatis ke program C. Untuk beberapa program, menggunakan kolektor Boehm bahkan dapat mempercepat. Ia bahkan dapat digunakan sebagai mekanisme deteksi kebocoran.

Kelemahan utama dari pengumpul sampah Boehm adalah ia tidak dapat memindai atau mengosongkan memori yang menggunakan default malloc. Ia menggunakan fungsi alokasinya sendiri, dan hanya bekerja pada memori yang Anda alokasikan secara khusus dengannya.

Jangan gunakan C saat bahasa lain bisa digunakan

Beberapa orang menulis dalam C karena mereka benar-benar menikmatinya dan menganggapnya bermanfaat. Secara keseluruhan, bagaimanapun, yang terbaik adalah menggunakan C hanya ketika Anda harus, dan kemudian hanya sedikit, untuk beberapa situasi di mana itu benar-benar adalah pilihan ideal.

Jika Anda memiliki proyek di mana kinerja eksekusinya akan dibatasi terutama oleh I / O atau akses disk, menulisnya dalam C kemungkinan tidak akan membuatnya lebih cepat dalam hal-hal yang penting, dan mungkin hanya akan membuatnya lebih rentan terhadap kesalahan dan sulit untuk dilakukan. mempertahankan. Program yang sama bisa juga ditulis dengan Go atau Python.

Pendekatan lain adalah dengan menggunakan C hanya untuk yang benar-benar kinerja-intensif bagian dari aplikasi, dan lebih dapat diandalkan meskipun bahasa lambat untuk bagian lain. Sekali lagi, Python dapat digunakan untuk membungkus pustaka C atau kode C kustom, menjadikannya pilihan yang baik untuk komponen boilerplate lainnya seperti penanganan opsi baris perintah.