私は、主にテキストで機能するプレーンC(c99)で新しいプロジェクトを開始しています。外部プロジェクトの制約があるため、このコードは非常にシンプルでコンパクトである必要があり、libcや同様のユビキタスシステムライブラリを除いて、外部の依存関係やライブラリを含まない単一のソースコードファイルで構成されます。
その理解の下で、プロジェクトの文字列処理をより堅牢で安全にするのに役立つベストプラクティス、落とし穴、トリック、またはその他のテクニックは何ですか?
私は、主にテキストで機能するプレーンC(c99)で新しいプロジェクトを開始しています。外部プロジェクトの制約があるため、このコードは非常にシンプルでコンパクトである必要があり、libcや同様のユビキタスシステムライブラリを除いて、外部の依存関係やライブラリを含まない単一のソースコードファイルで構成されます。
その理解の下で、プロジェクトの文字列処理をより堅牢で安全にするのに役立つベストプラクティス、落とし穴、トリック、またはその他のテクニックは何ですか?
コードが何をしているかについての追加情報がなければ、すべてのインターフェイスを次のように設計することをお勧めします。
size_t foobar(char *dest, size_t buf_size, /* operands here */)
のようなセマンティクスでsnprintf
:
dest
サイズが少なくとも のバッファを指しbuf_size
ます。buf_size
がゼロの場合、null/無効なポインターは受け入れられ、dest
何も書き込まれません。buf_size
がゼロでない場合dest
は、常にnull で終了します。foobar
は、切り捨てられていない完全な出力の長さを返します。buf_size
が戻り値以下の場合、出力は切り捨てられています。このようにして、呼び出し元が必要な宛先バッファーのサイズを簡単に知ることができる場合、事前に十分な大きさのバッファーを取得できます。呼び出し元が簡単に知ることができない場合は、 の引数をゼロにするか、buf_size
「おそらく十分な大きさ」のバッファーを使用して関数を 1 回呼び出し、スペースが不足した場合にのみ再試行することができます。
GNU 関数に似た呼び出しのラップ バージョンを作成することもできますがasprintf
、コードをできるだけ柔軟にしたい場合は、実際の文字列関数で割り当てを行うことは避けます。失敗の可能性を処理することは、呼び出し元レベルで常に簡単です。また、多くの呼び出し元は、ローカル バッファーまたはプログラムのずっと前に取得されたバッファーを使用して、失敗の可能性が決してないようにすることができるため、より大きな操作の成功または失敗を確認できます。アトミックです (これにより、エラー処理が大幅に簡素化されます)。
長年の組み込み開発者からのいくつかの考え。そのほとんどは、単純さの要件について詳しく説明しており、C 固有のものではありません。
必要な文字列処理関数を決定し、そのセットをできるだけ小さくして、障害点を最小限に抑えます。
R. の提案に従って、すべての文字列ハンドラーで一貫した明確なインターフェイスを定義してください。厳密で小さいながらも詳細な一連のルールにより、パターン マッチングをデバッグ ツールとして使用できます。他のコードとは異なるように見えるコードを疑うことができます。
Bart van Ingen Schenau が指摘したように、文字列の長さとは無関係にバッファーの長さを追跡します。常にテキストを扱う場合は、標準の null 文字を使用して文字列の終わりを示すのが安全ですが、テキスト + null がバッファーに収まるようにするのはあなた次第です。
すべての文字列ハンドラーで一貫した動作を保証します。特に標準関数が不足している場合: 切り捨て、null 入力、null 終了、パディングなど。
どうしてもルールに違反する必要がある場合は、その目的のために別の関数を作成し、適切な名前を付けます。つまり、各関数に単一の明確な動作を与えます。str_copy_and_pad()
そのため、常にターゲットにヌルを埋め込む関数に使用できます。
可能な限り、安全な組み込み関数 (例: memmove()
Jonathan Leffler による) を使用して重い作業を行ってください。しかし、彼らがしていると思うことを彼らがしていることを確認するためにそれらをテストしてください!
できるだけ早くエラーを確認してください。検出されないバッファ オーバーランは、見つけにくいことで有名な「跳弾」エラーにつながる可能性があります。
すべての関数のテストを作成して、そのコントラクトを満たしていることを確認します。エッジ ケース (off by 1、null/空の文字列、ソース/宛先のオーバーラップなど) を必ずカバーしてください。これは明白に聞こえるかもしれませんが、バッファー アンダーラン/オーバーランを作成して検出する方法を理解し、テストを作成するようにしてください。それらの問題を明示的に生成してチェックします。(私の QA 担当者は、「動作することを確認するためだけにテストするのではなく、壊れないことを確認するためにテストする」という私の指示にうんざりしているでしょう。)
ここに私のために働いたいくつかのテクニックがあります:
割り当て時にバッファの両端に「フェンス バイト」を割り当て、割り当て解除時にチェックするメモリ管理ルーチンのラッパーを作成します。おそらくSTR_DEBUGマクロが設定されている場合、文字列ハンドラー内でそれらを確認することもできます。 警告: 診断を徹底的にテストして、追加の障害点が作成されないようにする必要があります。
バッファーとその長さの両方をカプセル化するデータ構造を作成します。(フェンス バイトを使用する場合は、それらを含めることもできます。) 警告: コード ベース全体で管理する必要がある非標準のデータ構造が存在するため、大幅な書き直しが必要になる可能性があります (したがって、追加の障害点が発生する可能性があります)。
文字列ハンドラーが入力を検証するようにします。関数が null ポインターを禁止している場合は、明示的に確認してください。有効な文字列が必要で ( strlen()
should のように)、バッファーの長さがわかっている場合は、バッファーに null 文字が含まれていることを確認してください。言い換えれば、コードまたはデータに関して行っている可能性のある仮定を検証します。
最初にテストを書きます。これは、各関数のコントラクトを理解するのに役立ちます。つまり、関数が呼び出し元に何を期待しているか、呼び出し元が何を期待すべきかを正確に理解するのに役立ちます。それをどのように使用するか、どのように壊れる可能性があるか、処理しなければならない特殊なケースについて考えていることに気付くでしょう。
この質問をしてくれてありがとう!特にコーディングを始める前に、より多くの開発者がこれらの問題について考えてくれることを願っています。頑張って、堅牢で成功した製品をお祈りします!
strlcpy
およびstrlcat
を参照してください。詳細については、 を参照してoriginal paper
ください。
2セント:
最大サイズに達した場合、文字列関数の「n」バージョンはコピー後に「\ 0」を追加しないため、最後のステップは重要です。
可能な限りスタック上の配列を操作し、適切に初期化します。割り当て、サイズ、初期化を追跡する必要はありません。
char myCopy[] = { "the interesting string" };
中サイズの文字列の場合、C99 には VLA があります。初期化できないため、少し使いにくいです。ただし、上記の利点のうち最初の 2 つを引き続き利用できます。
char myBuffer[n];
myBuffer[0] = '\0';
いくつかの重要な落とし穴は次のとおりです。
'\0'
ます。この文字がその文字列用に予約されたバッファ内にあることを確認するのは、プログラマの責任です。時間対空間に関しては、ここから標準ビットいじりを選択することを忘れないでください
初期のファームウェア プロジェクトでは、ルックアップ テーブルを使用して、O(1) 操作効率で設定されたビットを数えました。