Cライブラリの関数は常に文字列の長さを期待すべきですか?


15

私は現在、Cで書かれたライブラリに取り組んでいます。このライブラリの多くの関数は、引数として、char*またはconst char*引数に文字列が必要です。私は、文字列の長さを常に期待しているsize_tので、ヌル終端が必要ないようにそれらの関数から始めました。ただし、テストを作成する場合strlen()、次のようにが頻繁に使用されます。

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

適切に終了した文字列を渡すようにユーザーを信頼すると、安全性は低下しますが、より簡潔で(私の意見では)読み取り可能なコードになります。

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

だから、ここで賢明な慣行は何ですか?APIの使用をより複雑にしますが、ユーザーに入力を考えさせるか、ヌル終了文字列の要件を文書化し、呼び出し元を信頼しますか?

回答:


4

最も間違いなく絶対に長さを運んでください。標準Cライブラリはこの方法で悪名高く壊れており、バッファオーバーフローの処理に苦痛はありません。このアプローチは、現代のコンパイラがこの種の標準ライブラリ関数を使用するときに実際に警告、泣き言、不満を言うほどの憎しみと苦悩の焦点です。

とても悪いので、インタビューでこの質問に出くわすと、テクニカルインタビュアーが数年の経験を積んでいるように見えます。C文字列ターミネーターを探しているAPIを実装している人を撃った先例。

感情はさておき、文字列の最後にあるNULLを読むことと操作することの両方で、間違っている可能性があります。さらに、多層防御などの現代の設計概念に直接違反しています。 (必ずしもセキュリティに適用されるわけではありませんが、APIデザインに適用されます)。長さが多いC APIの例-例 Windows API。

実際、この問題は90年代に解決されました。今日の新たなコンセンサスは、弦に触れてはいけないということです

後の編集:これは非常に活発な議論なので、下や上のすべての人が素敵でライブラリstr *関数を使用することを信頼することは、output = malloc(strlen(input)); strcpy(output, input);またはのような古典的なものが表示されるまでは大丈夫ですwhile(*src) { *dest=transform(*src); dest++; src++; }。背景にはモーツァルトのラクリモサがほとんど聞こえます。


1
呼び出し側に文字列の長さを提供することを要求するWindows APIの例がわかりません。たとえば、などの典型的なWin32 API関数CreateFileは、LPTCSTR lpFileName入力としてパラメーターを受け取ります。呼び出し元からの文字列の長さはありません。実際、NULで終了する文字列の使用は非常に深く根付いているため、ドキュメントでは、ファイル名をNULで終了する必要があることについて言及していません(もちろんそうする必要があります)。
グレッグヒューギル

1
実際のWin32で、LPSTRタイプは文字列はと言うかもしれ NULが-終了し、あれば可能ではない、それは、関連する仕様書に表示されます。したがって、特に明記されていない限り、Win32のこのような文字列はNULで終了することが期待されます。
グレッグ

素晴らしい点、私は不正確でした。CreateFileとその束は、Windows NT 3.1(90年代前半)以降のものであると考えてください。現在のAPI(つまり、XP SP2でStrsafe.hが導入されて以来-Microsoftの公開謝罪)は、可能な限りすべてのNULLで終了したものを明示的に廃止しました。MicrosoftがNULLで終了する文字列を使用して本当に本当に申し訳ないと感じたのは、VB、COM、古いWINAPIを同じボートに入れるために、OLE 2.0仕様でBSTRを導入しなければならなかったときです。
vski

1
StringCbCat例えば、唯一の宛先は理にかなって最大バッファを有しています。ソースはまだ通常のNULで終了するC文字列です。おそらく、入力パラメーターと出力パラメーターの違いを明確にすることで、答えを改善できます。出力パラメーターには常に最大バッファー長が必要です。通常、入力パラメーターはNULで終了します(例外はありますが、私の経験ではまれです)。
グレッグ

1
はい。文字列は、JVM / Dalvikと.NET CLRの両方でプラットフォームレベルで、および他の多くの言語で不変です。私はこれまでのところ、ネイティブの世界はまだこれを行うことができないと推測します(C ++ 11標準)a)レガシー(文字列の一部だけを不変にすることでそれほど多くを得ることはありません)とb )この機能を実現するには、GCと文字列テーブルが本当に必要です。C++ 11のスコープ付きアロケーターでは、これを完全に削減することはできません。
vski

16

Cでは、文字列はNULで終了するというイディオムですので、一般的な慣行に従うことは理にかなっています-実際には、ライブラリのユーザーがNULで終了していない文字列を持つことは比較的ありません(印刷するには余分な作業が必要です) printfを使用し、他のコンテキストで使用します)。他の種類の文字列を使用するのは不自然で、おそらく比較的まれです。

また、この状況では、テストは少し奇妙に見えます。正しく動作するため(strlenを使用)、最初はNULで終了する文字列を想定しているからです。ライブラリでそれらを使用する場合は、NULで終了しない文字列のケースをテストする必要があります。


-1、すみません、これは単に不適切です。
vski

昔は、これは必ずしも真実ではなかった。NULLで終端されていない固定長フィールドに文字列データを配置するバイナリプロトコルで多くの作業をしました。そのような場合、長い時間がかかった関数を扱うのは非常に手間がかかりました。しかし、10年でCをやったことはありません。
ロボット

4
@vski、ターゲット関数を呼び出す前にユーザーに「strlen」を呼び出すように強制すると、バッファオーバーフローの問題を回避するために何ができますか?少なくとも、ターゲット関数内で自分で長さをチェックすれば、どの長さの感覚が使用されているか(端末のヌルを含むかどうか)を確信できます。
チャールズE.グラント

@Charles E. Grant:Strsafe.hのStringCbCatおよびStringCbCatNに関する上記のコメントを参照してください。char *だけで長さがない場合、実際にはstr *関数を使用する以外に実際の選択肢はありませんが、ポイントは長さを運ぶことであるため、str *とstrn *の間のオプションになります後者が好ましい機能。
vski

2
@vski 文字列の長さを渡す必要はありません。そこ周りに渡す必要バッファの長さ。すべてのバッファが文字列ではなく、すべての文字列がバッファではありません。
ジェームズリン

10

あなたの「安全」論は本当に成り立ちません。それが文書化されているときにヌル終端文字列を渡すことをユーザーに信頼していない場合(およびプレーンCの「標準」)、彼らがあなたに与える長さを本当に信頼することはできませんおそらくstrlen、彼らがそれを手元に持っていない場合、あなたがしているのと同じように使用することで得られます。

ただし、長さを必要とする正当な理由があります:関数を部分文字列で動作させたい場合、長さを渡すほうが、ユーザーにいくつかのマジックを行ったり来たりしてヌルバイトを取得させるよりもはるかに簡単です適切な場所で(そして途中で1つずつエラーが発生するリスクがあります)。
nullバイトが終端ではないエンコーディングを処理できること、または(意図的に)nullが埋め込まれた文字列を処理できることは、状況によっては便利な場合があります(関数の正確な動作によって異なります)。
NULLで終了しないデータ(固定長配列)を処理できることも便利です。
つまり、ライブラリで何をしているのか、そしてユーザーがどのタイプのデータを処理すると予想されるのかによって異なります。

これにはパフォーマンスの面もあります。関数が文字列の長さを事前に知る必要があり、ユーザーが少なくとも通常その情報を知っていることを期待する場合、(計算するのではなく)渡すことで数サイクルを削ることができます。

ただし、ライブラリが通常のプレーンASCIIテキスト文字列を想定しており、パフォーマンスの制約が厳しくなく、ユーザーがライブラリを操作する方法を十分に理解していない場合、長さパラメーターを追加するのは良い考えではありません。文字列が適切に終了していない場合、長さパラメーターが同じように偽になる可能性があります。私はあなたがそれで多くを得るとは思わない。


このアプローチには強く反対します。特にライブラリAPIの背後にいる呼び出し元を決して信用しないでください。かっこいい長さを持ち、NULLで終了する文字列を操作することは、「発信者にはゆるく、着信者には厳しい」という意味ではありません。
vski

2
私はにあなたの立場に同意しますが、あなたはその長さの議論に多くの信頼を置いているようです-それがヌルターミネーターより信頼できるはずの理由はありません。私の立場は、図書館が何をするかにかかっているということです。
マット

値で渡される長さよりも、文字列のNULLターミネータの方が間違っている可能性があります。Cでは、長さを信頼する唯一の理由は、それが不合理で非実用的ではないためです-バッファ長を運ぶことは良い答えではなく、代替案を考慮する最良の方法です。文字列(および一般的なバッファ)がきちんとパックされ、RAD言語でカプセル化される理由の1つです。
vski

2

いいえ。定義上、文字列は常にヌルで終了します。文字列の長さは冗長です。

NULLで終了しない文字データは、「文字列」と呼ばれることはありません。通常、それを処理(および長さをスロー)するの、APIの一部ではなく、ライブラリ内にカプセル化する必要があります。単一のstrlen()呼び出しを避けるためだけにパラメーターとして長さを要求することは、おそらく早期最適化です。

API関数の呼び出し元を信頼することは安全ではありませ。文書化された前提条件が満たされない場合、未定義の動作は完全に問題ありません。

もちろん、適切に設計されたAPIには落とし穴が含まれていてはならず、正しく使用しやすくする必要があります。そしてこれは、冗長性を回避し、言語の慣習に従うことで、できるだけシンプルで単純でなければならないことを意味します。


完全に問題がないだけでなく、メモリセーフなシングルスレッド言語に移行しない限り、実際には避けられません。いくつかのより多くのneccessary制限が...落ちたかもしれない
デュプリケータ

1

常に長さを保つ必要があります。1つには、ユーザーにNULLを含めることができます。次に、それstrlenがO(N)であり、文字列全体のキャッシュに触れる必要があることを忘れないでください。そして第三に、サブセットを簡単にやり取りできるようにします。たとえば、実際の長さよりも短いサブセットを指定できます。


4
ライブラリ関数が文字列に埋め込まれたNULLを処理するかどうかは、十分に文書化する必要があります。ほとんどのCライブラリ関数は、NULLまたは長さのどちらか早い方で停止します。(そして、有能に書かれていれば、長さがかからないものstrlenはループテストで使用しません。)
ロボットを

1

文字列を渡すこととバッファを渡すことを区別する必要があります

Cでは、文字列は伝統的にNULで終了します。これを期待することは完全に合理的です。したがって、通常、文字列の長さを渡す必要はありません。strlen必要に応じて計算できます。

周りに渡すときはバッファに書き込まれ、特に1を、あなたは絶対にバッファサイズに沿って渡す必要があります。宛先バッファーの場合、これにより、呼び出し先はバッファーがオーバーフローしないことを確認できます。入力バッファの場合、特に入力バッファに信頼できないソースから発信された任意のデータが含まれている場合、呼び出し先が終わりを超えて読み取ることを回避できます。

文字列とバッファの両方が存在する可能性がchar*あり、多くの文字列関数が宛先バッファに書き込むことで新しい文字列を生成するため、おそらく多少の混乱があります。その後、一部の人々は、文字列関数は文字列の長さを取る必要があると結論付けています。ただし、これは不正確な結論です。バッファにサイズを含める慣習(バッファを文字列、整数の配列、構造などに使用するかどうか)は、より有用で一般的なマントラです。

(信頼できないソース(ネットワークソケットなど)から文字列を読み取る場合、入力がNULで終了していない可能性があるため、長さを指定することが重要です。 ただし、入力を文字列と見なさないでください。文字列を含む可能性のある任意のデータバッファとして処理する必要があります(ただし、実際に検証するまでわかりません)。したがって、バッファにはサイズが関連付けられている必要があり、文字列には必要ないという原則に従います。


これはまさに質問や他の回答が逃したものです。
Blrfl

0

関数が主に文字列リテラルで使用される場合、いくつかのマクロを定義することにより、明示的な長さを扱う苦痛を最小限に抑えることができます。たとえば、API関数が与えられた場合:

void use_string(char *string, int length);

マクロを定義できます:

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

次に、次のように呼び出します。

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

コンパイルされるが実際には機能しないマクロを渡すための「創造的な」ものを考え出すことは可能かもしれませんが""、「sizeof」の評価内で文字列の両側での使用は、文字を使用する偶発的な試みをキャッチする必要があります分解された文字列リテラル以外のポインター[これらが存在しない場合""、文字ポインターを渡そうとすると、誤ってポインターのサイズから1を引いた長さが与えられます。

C99の代替アプローチは、「ポインターと長さ」構造タイプを定義し、文字列リテラルをその構造タイプの複合リテラルに変換するマクロを定義することです。例えば:

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

そのようなアプローチを使用する場合、そのような構造体をアドレスで渡すのではなく、値で渡す必要があることに注意してください。それ以外の場合:

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

複合リテラルの有効期間は、それを囲むステートメントの終わりで終了するため、失敗する可能性があります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.