Haruskah fungsi pustaka C selalu mengharapkan panjang string?

15

Saat ini saya sedang mengerjakan perpustakaan yang ditulis dalam C. Banyak fungsi perpustakaan ini mengharapkan string sebagai char*atau const char*dalam argumen mereka. Saya mulai dengan fungsi-fungsi itu selalu mengharapkan panjang string size_tsehingga null-termination tidak diperlukan. Namun, saat menulis tes, ini mengakibatkan sering digunakan strlen(), seperti:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Memercayai pengguna untuk mengirimkan string yang diakhiri dengan benar akan menyebabkan kode menjadi kurang aman, tetapi lebih ringkas dan (menurut saya):

libFunction("I hope there's a null-terminator there!");

Jadi, apa praktik yang masuk akal di sini? Membuat API lebih rumit untuk digunakan, tetapi memaksa pengguna untuk memikirkan input mereka, atau mendokumentasikan persyaratan untuk string yang diakhiri dengan nol dan mempercayai penelepon?

Benjamin Kloster
sumber

Jawaban:

4

Paling pasti dan benar-benar membawa panjang sekitar . Pustaka C standar terkenal rusak dengan cara ini, yang telah menyebabkan rasa sakit tanpa akhir dalam menangani buffer overflows. Pendekatan ini adalah fokus dari begitu banyak kebencian dan kesedihan sehingga kompiler modern benar-benar akan memperingatkan, merengek dan mengeluh ketika menggunakan fungsi perpustakaan standar semacam ini.

Sangat buruk, bahwa jika Anda pernah menemukan pertanyaan ini di sebuah wawancara - dan pewawancara teknis Anda sepertinya memiliki beberapa tahun pengalaman - kefanatikan murni dapat mendaratkan pekerjaan - Anda sebenarnya bisa mendapatkan cukup jauh ke depan jika Anda dapat mengutip preseden menembak seseorang yang mengimplementasikan API mencari terminator string C.

Mengesampingkan emosi itu semua, ada banyak yang bisa salah dengan NULL itu di akhir string Anda, baik dalam membaca dan memanipulasi itu - plus itu benar-benar melanggar langsung konsep desain modern seperti pertahanan mendalam (tidak harus diterapkan pada keamanan, tetapi untuk desain API). Contoh API C yang membawa panjang berlimpah - mis. API Windows.

Bahkan, masalah ini diselesaikan sekitar tahun 90-an, konsensus yang muncul saat ini adalah bahwa Anda bahkan tidak boleh menyentuh senar Anda .

Sunting nanti : ini adalah debat langsung jadi saya akan menambahkan bahwa mempercayai semua orang di bawah dan di atas Anda untuk bersikap baik dan menggunakan fungsi pustaka str * tidak masalah, hingga Anda melihat hal-hal klasik seperti output = malloc(strlen(input)); strcpy(output, input);atau while(*src) { *dest=transform(*src); dest++; src++; }. Saya hampir bisa mendengar Mozart's Lacrimosa di latar belakang.

vski
sumber
1
Saya tidak mengerti contoh Anda tentang Windows API yang membutuhkan penelepon untuk memasok panjang string. Misalnya, fungsi Win32 API khas seperti CreateFilemengambil LPTCSTR lpFileNameparameter sebagai input. Panjang string tidak diharapkan dari penelepon. Bahkan, penggunaan string yang diakhiri dengan NUL begitu mendarah daging sehingga dokumentasi bahkan tidak menyebutkan bahwa nama file harus diakhiri dengan NUL (tapi tentu saja harus).
Greg Hewgill
1
Sebenarnya di Win32, LPSTRtipe mengatakan bahwa string dapat diakhiri dengan NUL, dan jika tidak , itu akan ditunjukkan dalam spesifikasi yang terkait. Jadi kecuali jika secara khusus ditunjukkan sebaliknya, string seperti itu di Win32 diharapkan akan dihentikan NUL.
Greg Hewgill
Poin bagus, saya tidak tepat. Pertimbangkan bahwa CreateFile dan kumpulannya ada sejak Windows NT 3.1 (awal 90-an); API saat ini (yaitu sejak diperkenalkannya Strsafe.h di XP SP2 - dengan permintaan maaf publik Microsoft) secara eksplisit mencabut semua hal yang diakhiri dengan NULL. Pertama kali Microsoft merasa sangat menyesal karena menggunakan string yang diakhiri NULL sebenarnya jauh lebih awal, ketika mereka harus memperkenalkan BSTR dalam spesifikasi OLE 2.0, untuk entah bagaimana membawa VB, COM dan WINAPI lama di kapal yang sama.
vski
1
Bahkan dalam StringCbCatmisalnya, hanya tujuan yang memiliki buffer maksimum, yang masuk akal. The sumber masih merupakan NUL-dihentikan string C biasa. Mungkin Anda bisa meningkatkan jawaban Anda dengan mengklarifikasi perbedaan antara parameter input dan parameter output . Parameter output harus selalu memiliki panjang buffer maksimum; parameter input biasanya diakhiri NUL (ada pengecualian, tapi jarang dalam pengalaman saya).
Greg Hewgill
1
Iya. String tidak dapat diubah pada JVM / Dalvik dan. NET CLR di tingkat platform, serta dalam banyak bahasa lainnya. Saya akan melangkah lebih jauh dan berspekulasi bahwa dunia asli belum bisa melakukan ini (standar C ++ 11) karena a) warisan (Anda tidak benar-benar mendapatkan sebanyak itu dengan hanya memiliki sebagian string Anda yang tidak dapat diubah) dan b ) Anda benar-benar membutuhkan GC dan tabel string untuk membuat pekerjaan ini, pengalokasi scoped di C ++ 11 tidak bisa memotongnya.
vski
16

Dalam C, idiomnya adalah bahwa string karakter diakhiri NUL, jadi masuk akal untuk mematuhi praktik umum - sebenarnya relatif tidak mungkin bahwa pengguna perpustakaan akan memiliki string yang diakhiri non-NUL (karena ini membutuhkan kerja ekstra untuk mencetak menggunakan printf dan gunakan dalam konteks lain). Menggunakan string jenis apa pun tidak wajar dan mungkin relatif jarang.

Selain itu, dalam keadaan ini, pengujian Anda terlihat sedikit aneh bagi saya, karena berfungsi dengan benar (menggunakan strlen), Anda mengasumsikan string yang diakhiri NUL. Anda harus menguji kasus string non-NUL jika Anda bermaksud perpustakaan Anda untuk bekerja dengannya.

James McLeod
sumber
-1, saya minta maaf, ini hanya keliru.
vski
Di masa lalu, ini tidak selalu benar. Saya banyak bekerja dengan protokol biner yang menempatkan data string dalam bidang panjang tetap yang tidak dihentikan NULL. Dalam kasus seperti itu, sangat sulit untuk bekerja dengan fungsi yang membutuhkan waktu lama. Saya belum melakukan C dalam satu dekade, meskipun.
Gort the Robot
4
@vski, bagaimana cara memaksa pengguna untuk memanggil 'strlen' sebelum memanggil fungsi target melakukan apa saja untuk menghindari masalah buffer overflow? Setidaknya jika Anda memeriksa sendiri panjangnya dalam fungsi target Anda dapat yakin tentang panjang yang digunakan (termasuk terminal nol atau tidak).
Charles E. Grant
@Charles E. Grant: Lihat komentar di atas tentang StringCbCat dan StringCbCatN di Strsafe.h. Jika Anda hanya memiliki karakter * dan tanpa panjang, maka memang Anda tidak punya pilihan selain menggunakan fungsi str *, tetapi intinya adalah untuk membawa-panjang-sekitar, sehingga menjadi pilihan antara str * dan strn * fungsi yang lebih disukai.
vski
2
@vski Tidak perlu melewati panjang string . Ada adalah kebutuhan untuk lulus sekitar penyangga panjang 's. Tidak semua buffer adalah string, dan tidak semua string adalah buffer.
jamesdlin
10

Argumen "keamanan" Anda tidak benar-benar berlaku. Jika Anda tidak memercayai pengguna untuk memberikan Anda string yang diakhiri dengan nol saat itu yang Anda dokumentasikan (dan apa itu "norma" untuk plain C), Anda tidak bisa mempercayai panjang yang mereka berikan kepada Anda (yang akan mereka berikan) mungkin dapatkan dengan menggunakan strlensama seperti yang Anda lakukan jika mereka tidak memiliki itu berguna, dan yang akan gagal jika "string" bukan string di tempat pertama).

Ada alasan yang sah untuk membutuhkan panjang: jika Anda ingin fungsi Anda bekerja pada substring, mungkin jauh lebih mudah (dan efisien) untuk melewati panjang daripada meminta pengguna melakukan beberapa sihir penyalin bolak-balik untuk mendapatkan byte nol di tempat yang tepat (dan risiko kesalahan satu per satu di sepanjang jalan).
Mampu menangani penyandian di mana null byte tidak diakhiri, atau mampu menangani string yang telah menyematkan nulls (dengan sengaja) dapat berguna dalam beberapa keadaan (tergantung pada apa tepatnya fungsi Anda lakukan).
Mampu menangani data non-null yang dihentikan (array dengan panjang tetap) juga berguna.
Singkatnya: tergantung pada apa yang Anda lakukan di perpustakaan Anda, dan jenis data apa yang Anda harapkan akan ditangani oleh pengguna Anda.

Mungkin juga ada aspek kinerja untuk ini. Jika fungsi Anda perlu mengetahui panjang string terlebih dahulu, dan Anda berharap pengguna Anda setidaknya sudah tahu informasi itu, meminta mereka melewatinya (daripada menghitungnya) dapat mencukur beberapa siklus.

Tetapi jika perpustakaan Anda mengharapkan string teks ASCII biasa, dan Anda tidak memiliki kendala kinerja yang luar biasa dan pemahaman yang sangat baik tentang bagaimana pengguna Anda akan berinteraksi dengan perpustakaan Anda, menambahkan parameter panjang tidak terdengar seperti ide yang bagus. Jika string tidak diakhiri dengan benar, kemungkinan parameter panjangnya akan sama palsu. Saya tidak berpikir Anda akan mendapatkan banyak dengan itu.

Tikar
sumber
Sangat tidak setuju dengan pendekatan ini. Jangan pernah mempercayai penelepon Anda, terutama di balik API perpustakaan, berusaha sebaik mungkin untuk mempertanyakan hal-hal yang mereka berikan kepada Anda dan gagal dengan anggun. Membawa panjang terkutuk, bekerja dengan string yang diakhiri NULL bukanlah apa yang dimaksud "lepas dengan penelepon Anda dan ketat dengan betis Anda" berarti.
vski
2
Saya sebagian besar setuju dengan posisi Anda, tetapi Anda tampaknya menaruh banyak kepercayaan pada argumen panjang itu - tidak ada alasan mengapa itu harus dapat diandalkan daripada terminator nol. Posisi saya tergantung pada apa yang dilakukan perpustakaan.
Mat
Ada banyak lagi yang bisa salah dengan terminator NULL dalam string daripada dengan panjang melewati nilai. Dalam C, satu-satunya alasan seseorang akan mempercayai panjangnya adalah karena itu tidak masuk akal dan tidak praktis untuk tidak - membawa panjang buffer bukan jawaban yang baik, hanya yang terbaik mempertimbangkan alternatif. Ini adalah salah satu alasan mengapa string (dan buffer secara umum) dikemas dengan rapi dan dikemas dalam bahasa RAD.
vski
2

Tidak. String selalu diakhiri dengan nol menurut definisi, panjang string berlebihan.

Data karakter non-null yang dihentikan tidak boleh disebut "string". Memprosesnya (dan melemparkan panjang sekitar) biasanya harus dikemas dalam perpustakaan, dan bukan bagian dari API. Membutuhkan panjang sebagai parameter hanya untuk menghindari panggilan tunggal strlen () sepertinya adalah Pengoptimalan Dini.

Memercayai penelepon fungsi API tidak aman ; perilaku tidak terdefinisi adalah sangat baik jika prasyarat yang didokumentasikan tidak terpenuhi.

Tentu saja, API yang dirancang dengan baik seharusnya tidak mengandung jebakan dan membuatnya mudah untuk digunakan dengan benar. Dan ini hanya berarti harus sesederhana dan sejelas mungkin, menghindari redudansi dan mengikuti konvensi bahasa.

dpi
sumber
tidak hanya benar-benar baik-baik saja, tetapi sebenarnya tidak dapat dihindari kecuali seseorang beralih ke bahasa yang aman-memori, satu-utas. Mungkin telah membatalkan beberapa pembatasan yang diperlukan ...
Deduplicator
1

Anda harus selalu menjaga jarak. Untuk satu, pengguna Anda mungkin ingin mengandung NULL di dalamnya. Dan kedua, jangan lupa bahwa strlenO (N) dan perlu menyentuh seluruh cache by-bye cache. Dan ketiga, membuatnya lebih mudah untuk melewati subset-misalnya, mereka bisa memberi kurang dari panjang sebenarnya.

DeadMG
sumber
4
Apakah fungsi pustaka berhubungan dengan NULL yang tersemat dalam string perlu didokumentasikan dengan sangat baik. Sebagian besar fungsi pustaka C berhenti di NULL atau panjangnya, mana yang lebih dulu. (Dan jika ditulis dengan kompeten, yang tidak membutuhkan waktu lama tidak pernah digunakan strlendalam tes loop.)
Gort the Robot
1

Anda harus membedakan antara melewatkan string dan melewati buffer .

Dalam C, string secara tradisional diakhiri dengan NUL. Sangat masuk akal untuk mengharapkan ini. Oleh karena itu biasanya tidak perlu melewati panjang tali; itu dapat dihitung dengan strlenjika perlu.

Ketika melewati buffer , terutama yang ditulis untuk, maka Anda harus benar-benar melewati ukuran buffer. Untuk buffer tujuan, ini memungkinkan callee untuk memastikan bahwa buffer tidak meluap. Untuk buffer input, ini memungkinkan callee untuk menghindari membaca melewati akhir, terutama jika buffer input berisi data acak yang berasal dari sumber yang tidak dipercaya.

Mungkin ada beberapa kebingungan karena baik string dan buffer bisa jadi char*dan karena banyak fungsi string menghasilkan string baru dengan menulis ke buffer tujuan. Beberapa orang kemudian menyimpulkan bahwa fungsi string harus mengambil panjang string. Namun, ini adalah kesimpulan yang tidak akurat. Praktek memasukkan ukuran dengan buffer (apakah buffer digunakan untuk string, array integer, struktur, apa pun) adalah mantra yang lebih berguna dan lebih umum.

(Dalam hal membaca string dari sumber yang tidak terpercaya (misalnya soket jaringan), penting untuk memasok panjang karena input mungkin tidak diakhiri dengan NUL. Namun , Anda tidak boleh menganggap input sebagai string. Anda harus memperlakukannya sebagai buffer data sewenang-wenang yang mungkin berisi string (tetapi Anda tidak tahu sampai Anda benar-benar memvalidasinya), jadi ini masih mengikuti prinsip bahwa buffer harus memiliki ukuran terkait dan bahwa string tidak memerlukannya.)

jamesdlin
sumber
Inilah tepatnya pertanyaan yang terjawab dan jawaban lainnya.
Blrfl
0

Jika fungsi terutama digunakan dengan string literal, rasa sakit berurusan dengan panjang eksplisit dapat diminimalkan dengan mendefinisikan beberapa makro. Misalnya, diberi fungsi API:

void use_string(char *string, int length);

seseorang dapat mendefinisikan makro:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

dan kemudian aktifkan seperti yang ditunjukkan pada:

void test(void)
{
  use_strlit("Hello");
}

Meskipun dimungkinkan untuk membuat hal-hal "kreatif" untuk melewati makro yang akan dikompilasi tetapi tidak akan benar-benar berfungsi, penggunaan ""di kedua sisi string dalam evaluasi "sizeof" harus menangkap upaya tak disengaja untuk menggunakan karakter pointer selain string literal yang diurai [jika tidak ada string itu "", upaya untuk melewatkan pointer karakter akan keliru memberikan panjang sebagai ukuran pointer, minus satu.

Pendekatan alternatif dalam C99 akan mendefinisikan tipe struktur "pointer dan panjang" dan mendefinisikan makro yang mengubah string literal menjadi senyawa majemuk dari tipe struktur itu. Sebagai contoh:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Perhatikan bahwa jika seseorang menggunakan pendekatan seperti itu, ia harus melewati struktur tersebut dengan nilai daripada melewati alamat mereka. Kalau tidak, sesuatu seperti:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

mungkin gagal karena masa pakai majemuk literal akan berakhir pada akhir pernyataan terlampir mereka.

supercat
sumber