web-dev-qa-db-ja.com

printf(s)とprintf( "%s"、s)の根本的な違いは何ですか?

質問は単純明快です。sは文字列です。突然、printf(s)を使用して動作するかどうかを確認しようと思いましたが、ある場合には警告が表示されましたが、もう1つ。

_char* s = "abcdefghij\n";
printf(s);

// Warning raised with gcc -std=c11: 
// format not a string literal and no format arguments [-Wformat-security]

// On the other hand, if I use 

char* s = "abc %d efg\n";
printf(s, 99);

// I get no warning whatsoever, why is that?

// Update, I've tested this:
char* s = "random %d string\n";
printf(s, 99, 50);

// Results: no warning, output "random 99 string".
_

では、printf(s)printf("%s", s)の根本的な違いは何ですか?また、1つのケースで警告が表示されるのはなぜですか?

27
Mikael

最初のケースでは、非リテラル形式の文字列は、ユーザーコードまたはユーザー提供の(実行時)データから取得される可能性があります。その場合、_%s_またはその他の変換仕様が含まれている可能性があります。データを渡しました。これにより、あらゆる種類の読み取りの問題が発生する可能性があります(文字列に_%n_が含まれている場合は書き込みの問題— printf() またはCライブラリのマニュアルページを参照)。

2番目のケースでは、フォーマット文字列が出力を制御し、出力される文字列に変換仕様が含まれているかどうかは関係ありません(ただし、示されているコードは文字列ではなく整数を出力します)。コンパイラー(質問ではGCCまたはClangが使用されます)は、(非リテラル)フォーマット文字列の後に引数があるため、プログラマーはそれらが何をしているのかを知っていると想定します。

1つ目は、「フォーマット文字列」の脆弱性です。トピックに関する詳細情報を検索できます。

GCCは、ほとんどの場合、非リテラル形式の文字列を含む単一の引数printf()が問題の原因であることを認識しています。代わりにputs()またはfputs()を使用できます。 GCCが最小限の挑発で警告を生成することは十分に危険です。

非リテラル形式の文字列のより一般的な問題は、注意しないと問題になる可能性がありますが、注意していると仮定すると非常に便利です。 GCCに苦情を申し立てるには、もっと努力する必要があります。苦情を申し立てるには、_-Wformat_と_-Wformat-nonliteral_の両方が必要です。

コメントから:

したがって、警告を無視すると、自分が何をしているかを本当に理解していてエラーが発生しないかのように、どちらかがより効率的に使用できますか、それとも同じですか?空間と時間の両方を考慮します。

3つのprintf()ステートメントのうち、変数sが呼び出しのすぐ上に割り当てられているという厳密なコンテキストを考えると、実際の問題はありません。ただし、文字列から改行を省略した場合はputs(s)を使用するか、fputs(s, stdout)をそのまま使用して、printf()全体を解析するオーバーヘッドなしで同じ結果を得ることができます。印刷されるのはすべて単純な文字であることを確認するための文字列。

2番目のprintf()ステートメントも記述どおりに安全です。フォーマット文字列は渡されたデータと一致します。それと単にフォーマット文字列をリテラルとして渡すこととの間に大きな違いはありません—コンパイラがフォーマット文字列がリテラルであるかどうかをさらにチェックできることを除いて。実行時の結果は同じです。

3番目のprintf()は、フォーマット文字列が必要とするよりも多くのデータ引数を渡しますが、それは問題ありません。しかし、それは理想的ではありません。繰り返しになりますが、コンパイラはフォーマット文字列がリテラルであるかどうかをより適切にチェックできますが、実行時の影響は実質的に同じです。

上部にリンクされているprintf()仕様から:

これらの各関数は、formatの制御下で引数を変換、フォーマット、および出力します。 formatは文字列であり、初期シフト状態(存在する場合)で開始および終了します。 formatは、0個以上のディレクティブで構成されます。出力ストリームに単純にコピーされる通常の文字と、変換仕様です。 0個以上の引数のフェッチ。フォーマットの引数が不十分な場合、結果は未定義です。引数が残っている間にフォーマットが使い果たされた場合、余分な引数は評価されますが、それ以外の場合は無視されます。

これらすべての場合において、フォーマット文字列がリテラルではない理由を強く示すものはありません。ただし、非リテラル形式の文字列が必要な理由の1つは、浮動小数点数を_%f_表記で出力する場合と_%e_表記で出力する場合があり、実行時にどちらを選択する必要があるかです。 (単に値に基づいている場合は、_%g_が適切な場合がありますが、明示的な制御が必要な場合があります—常に_%e_または常に_%f_。)

25

警告はそれをすべて言います。

まず、issueについて説明するために、署名に従って、printf()の最初のパラメーターはcanフォーマット指定子(変換指定子)が含まれています。 stringにフォーマット指定子が含まれていて、対応する引数が指定されていない場合、 未定義の動作 が呼び出されます。

したがって、cleaneror safer)アプローチ(フォーマット指定を必要としない文字列を印刷する) puts(s); over printf(s);前者は変換指定子のsを処理せず、UBの可能性のある理由を削除します。後の場合)。 fputs()に自動的に追加される末尾の改行が心配な場合は、puts()を選択できます。


とはいえ、警告オプションに関しては、_-Wformat-security_ オンラインgccマニュアルから

現在、これは、フォーマット文字列が文字列リテラルではなく、printfおよびscanf関数の呼び出しについて警告します。 printf (foo);のように、フォーマット引数はありません。フォーマット文字列が信頼できない入力からのものであり、_%n_が含まれている場合、これはセキュリティホールである可能性があります。

最初のケースでは、printf()に指定された引数は1つだけです。これは文字列リテラルではなく、変数であり、非常に適切に生成/入力できます。実行時に、unexpected形式指定子が含まれている場合、UBを呼び出す可能性があります。コンパイラには、その中にフォーマット指定子が存在するかどうかをチェックする方法がありません。それがセキュリティの問題です。

2番目のケースでは、付随する引数が指定され、フォーマット指定子はprintf()に渡されるonly引数ではないため、最初の引数を検証する必要はありません。 。したがって、警告はありません。


更新:

3番目のものに関しては、提供されたフォーマット文字列で必要なexcess引数を使用します

_printf(s, 99, 50);
_

_C11_、§7.21.6.1章からの引用

[...]引数が残っている間にフォーマットが使い果たされた場合、余分な引数は(いつものように)評価されますが、それ以外の場合は無視されます。 [...]

したがって、excess引数を渡すことは(コンパイラの観点からは)まったく問題ではなく、明確に定義されています。そこに警告の余地はありません。

6
Sourav Ghosh

あなたの質問には2つのことが関係しています。

最初のものは簡潔に Jonathan Leffler でカバーされています-あなたが得る警告は、文字列がリテラルではなく、フォーマット指定子が含まれていないためです。

もう1つは、引数の数が指定子の数と一致しないという警告をコンパイラーが発行しない理由の謎です。簡単な答えは「そうではないから」ですが、より具体的には、printfは可変個引数関数です。初期フォーマット指定の後、0から任意の数の引数を取ります。コンパイラは、適切な量を指定したかどうかを確認できません。これはprintf関数自体次第であり、Joachimがコメントで言及した未定義の動作につながります。

[〜#〜]編集[〜#〜]:小さな話に乗る手段として、あなたの質問にさらに答えます。ソープボックス。

printf(s)printf("%s", s)の違いは何ですか?単純-後者では、宣言されているとおりにprintfを使用しています。 "%s"constchar *であり、その後は警告メッセージを生成しません。

他の回答へのコメントで、「警告を無視する...」とおっしゃいました。これをしないでください。警告は理由があり、解決する必要があります(そうしないと、単なるノイズであり、そうでないすべての警告の中で実際に重要な警告を見逃してしまいます)。

問題はいくつかの方法で解決できます。

const char* s = "abcdefghij\n";
printf(s);

現在constポインターを使用しており、Jonathanが言及した危険はないため、は警告を解決します。 (const char* const sとして宣言することもできますが、必ずしもそうする必要はありません。最初のconstは重要です。これはprintfの宣言と一致し、const char* sは文字が指す文字を意味するためです。 sは変更できません。つまり、文字列はリテラルです。)

または、さらに簡単に、次のようにします。

printf("abcdefghij\n");

これは暗黙的にconstポインターであり、問​​題ではありません。

5

根本的な理由:printfは次のように宣言されています:

int printf(const char *fmt, ...) __attribute__ ((format(printf, 1, 2)));

これは、printfがフォーマット文字列が最初に来るprintfスタイルのインターフェイスを持つ関数であることをgccに伝えます。私見それは文字通りでなければなりません。 sが実際には以前に見たリテラル文字列へのポインタであることを、優れたコンパイラに伝える方法はないと思います。

__attribute__ここ についてもっと読む。

2

では、printf(s)とprintf( "%s"、s)の根本的な違いは何ですか

「printf(s)」はsをフォーマット文字列として扱います。 sにフォーマット指定子が含まれている場合、printfはそれらを解釈し、可変引数を探します。可変引数は実際には存在しないため、これは未定義の動作を引き起こす可能性があります。

攻撃者が「s」を制御している場合、これはセキュリティホールである可能性があります。

printf( "%s"、s)は、文字列の内容を出力するだけです。

なぜ1つのケースで警告が表示されるのですか?

警告は、危険な愚かさを捕らえることと、あまり多くの騒音を出さないことの間のバランスです。

Cプログラマーは、実際にフォーマットを必要としない場合でも、一般的な印刷関数としてprintfやさまざまなprintfのような関数*を使用する習慣があります。この環境では、誰かがprintfをどこから来たのかを考えずに、間違いを犯しがちです。 printfをフォーマットするためのデータがないと、フォーマットはほとんど役に立たないため、正当な用途はほとんどありません。

一方、printf(s、format、arguments)は、プログラマーが意図的にフォーマットを実行することを意図していたことを示します。

この警告は、アップストリームgccではデフォルトでオンになっていませんが、一部のディストリビューションでは、セキュリティホールを減らすための取り組みの一環としてオンになっています。

* sprintfやfprintfなどの標準C関数と、サードパーティライブラリの関数の両方。

2
plugwash