この指示:
if(*ptr)
cmd[strlen(cmd)]=' ';
else
cmd[strlen(cmd)]='\0';
cmd
ゼロ終端を上書きするため、破損します。代わりに試してください:
l = strlen(cmd);
if (*ptr) {
cmd[l++] = ' ';
}
cmd[l] = 0x0;
これによりスペースが追加され、文字列はゼロで終了します。実際には、それはすでにゼロで終了しているので、あなたはもっとうまくやることができます:
if (*ptr) {
int l = strlen(cmd);
cmd[l++] = ' ';
cmd[l ] = 0x0;
}
アップデート
より良い代替案はこれである可能性があります:
int main(int argc, char *argv[])
{
char cmd[255]="tcc/tcc.exe";
char **ptr=argv+1;
for (ptr = argv+1; *ptr; ptr++)
{
strncat(cmd, " ", sizeof(cmd)-strlen(cmd));
strncat(cmd, *ptr, sizeof(cmd)-strlen(cmd));
}
printf("String: '%s'.\n", cmd);
return 0;
}
バッファをstrncat()
オーバーランしていないことを確認するために使用し、スペースが事前に適用されます。cmd
このように、文字列の終わりに余分なスペースはありません。
strncat()
直接割り当てるよりもかなり遅いのは事実ですがcmd[]
、安全性とデバッグ時間を考慮すると、それは価値があると思います。
アップデート2
さて、これを速くやってみましょう。変数cmd
の長さを追跡し、文字列の長さをチェックせず、文字列の最後に余分なゼロをコピーするよりもわずかに速い文字列をコピーします。memcpy()
strcpy()
(これは何かを節約します-両方の引数strcat()
のを暗黙的に計算する必要があることを覚えておいてください。ここではそれを保存します)。strlen
int main(int argc, char *argv[])
{
#define MAXCMD 255
char cmd[MAXCMD]="tcc/tcc.exe";
int cmdlen = strlen(cmd);
char **ptr=argv+1;
for (ptr = argv+1; *ptr; ptr++)
{
/* How many bytes do we have to copy? */
int l = strlen(*ptr);
/* STILL, this check HAS to be done, or the program is going to crash */
if (cmdlen + 1 + l + 1 < MAXCMD)
{
/* No danger of crashing */
cmd[cmdlen++] = ' ';
memcpy(cmd + cmdlen, *ptr, l);
cmdlen += l;
}
else
{
printf("Buffer too small!\n");
}
}
cmd[cmdlen] = 0x0;
printf("String: '%s'.\n", cmd);
return 0;
}
アップデート3-あまりお勧めしませんが、楽しいです
コンパイラの通常の組み込みstrlen
およびmemcpy
命令(「悪いアイデア」の下のファイル)よりも賢く、strlen()をまったく使用せずに実行することは可能です。これは、より小さな内部ループに変換され、ライブラリ呼び出しを使用して実装されると、パフォーマンスが大幅に向上します(strlen
スタックフレームがないように見えます!)。memcpy
int main(int argc, char *argv[])
{
#define MAXCMD 254
char cmd[MAXCMD+1]="tcc/tcc.exe";
int cmdlen = 11; // We know initial length of "tcc/tcc.exe"!
char **ptr;
for (ptr = argv+1; *ptr; ptr++)
{
cmd[cmdlen++] = ' ';
while(**ptr) {
cmd[cmdlen++] = *(*ptr)++;
if (MAXCMD == cmdlen)
{
fprintf(stderr, "BUFFER OVERFLOW!\n");
return -1;
}
}
}
cmd[cmdlen] = 0x0;
printf("String: '%s'.\n", cmd);
return 0;
}
ディスカッション-それほど楽しくない
私が近視眼的だと思った教授から受けた多くの講義から恥知らずに、彼らが毎回正しいことが証明されるまで、恥ずかしがり屋でした。
ここでの問題は、私たちが何をしているのかを正確に制限することです。この特定の木が含まれている森は何ですか。
呼び出しに供給されるコマンドラインを構築しています。exec()
つまり、OSは別のプロセス環境を構築し、リソースを割り当てて追跡する必要があります。少し後退してみましょう。約1ミリ秒かかる操作が実行され、20マイクロ秒ではなく10マイクロ秒かかるループをフィードします。
内側のループでの20:10(50%!)の改善は、プロセスの起動操作全体だけで1020:1010(約1%)に変換されます。プロセスが完了するまでに0.5秒(500ミリ秒)かかると想像してみてください。十分に記憶されていないhttp://en.wikipedia.org/wikiによると、500020:500010または0.002%の改善が見られます。 / Amdahl%27s_law。
または、別の言い方をしましょう。それから1年後、私たちはこのプログラムを、たとえば10億回実行することになります。節約されたこれらの10マイクロ秒は、なんと10.000秒、つまり約2時間4分の3に相当します。この結果を得るために、コーディング、チェック、デバッグに16時間費やしたことを除けば、私たちは大々的に話し始めています:-)
ダブルstrncat()
ソリューション(実際には最も遅い)は、読みやすく、理解しやすく、変更しやすいコードを提供します。そして再利用します。上記の最速の解決策は、区切り文字が1文字であることに暗黙的に依存しており、この事実はすぐにはわかりません。つまり、区切り文字として「、」を使用して最速のソリューションを再利用すると(CSVまたはSQLでこれが必要だとしましょう)、微妙なバグが発生します。
アルゴリズムまたはコードの一部を設計するときは、コードのタイトさとローカル(「鍵穴」)のパフォーマンスだけでなく、次のようなものも考慮するのが賢明です。
- その単一のピースが全体のパフォーマンスにどのように影響するか。開発時間の10%を全体的な目標の10%未満に費やすことは意味がありません。
- コンパイラーがそれを解釈するのはどれほど簡単か(そして私たちの側でそれ以上の努力なしにそれを最適化する、おそらく異なるプラットフォームのために特別に最適化することさえ-すべて無料で!)
- 数日、数週間、または数か月後にそれを理解するのはどれほど簡単でしょうか。
- コードがどれほど非特異的で堅牢であり、どこか別の場所で再利用できるようにする(DRY)。
- その意図がどれほど明確であるか-後でそれを再設計したり、同じ意図の別の実装(DRAW)に置き換えたりすることができます。
微妙なバグ
これはWilliamMorrisの質問に答えているので、彼のコードを使用しますが、私の問題は同じです(実際、私の問題は-完全に意図せずに-はるかに悪いです)。
これは、Williamのコードの元の機能です。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
#define CMD "tcc/tcc.exe"
char cmd[255] = CMD;
char *s = cmd + sizeof CMD - 1;
const char *end = cmd + sizeof cmd - 1;
// Cycle syntax modified to pre-C99 - no consequences on our code
int i;
for (i = 1; i < argc; ++i) {
size_t len = strlen(argv[i]);
if (s + len >= end) {
fprintf(stderr, "Buffer overrun!\n");
exit(1);
}
// Here (will) be dragons
//*s++ = '.';
//*s++ = '.';
//*s++ = '.';
*s++ = ' ';
memcpy(s, argv[i], len);
s += len;
}
*s = '\0';
// Get also string length, which should be at most 254
printf("%s: string length is %d\n", cmd, (int)strlen(cmd));
return 0;
}
バッファオーバーランチェックは、これまでに書き込まれた文字列と、まだ書き込まれていない文字列が一緒になってバッファを超えていないことを確認します。セパレータ自体の長さはカウントされませんが、どういうわけかうまくいくでしょう:
size_t len = strlen(argv[i]);
if (s + len >= end) {
fprintf(stderr, "Buffer overrun!\n");
exit(1);
}
ここで、最も迅速な方法でセパレーターを追加します-ポークを繰り返すことによって:
*s++ = ', ';
*s++ = ' ';
s + len
がに等しい場合end - 1
、チェックは合格です。ここで、 2バイトを追加します。全長はs+len + 2になります。これは、end + 1に等しくなります:
tcc / tcc.exe、It、was、the、best、of、times、it、was、worst、of、times、it、was、the、age、of、wisdom、it、was、the、age、 of、foolishness、it、was、the、epoch、of、belief、it、was、the、epoch、of、incredulity、it、was、the、season、of、Light、it、was:string length is 254
tcc / tcc.exe、It、was、the、best、of、times、it、was、worst、of、times、it、was、the、age、of、wisdom、it、was、the、age、 of、愚かさ、it、was、the、epoch、of、belief、it、was、the、epoch、of、incredulity、it、was、the、season、of、Light、it、ouch:文字列の長さは255
「...」などの長いセパレータを使用すると、問題はさらに明白になります。
tcc /tcc.exe...それは...だった...だった...最高の...時間の...それは...だった...最悪の...の。 ..回...それは...だった...年齢...の...知恵...それは...だった......年齢...の...愚かさ...それは...だった...エポック...の...信念...それは...だった...もっと長い:文字列の長さは
257
私のバージョンでは、チェックに完全一致が必要であるという事実は壊滅的な結果につながります。バッファがオーバーランすると、一致は常に失敗し、大量のメモリが上書きされるためです。
私たちが私のバージョンを変更した場合
if (cmdlen >= MAXCMD)
バッファオーバーランを常にインターセプトするコードを取得しますが、それでも区切り文字の長さから2を引いた長さまでバッファオーバーランを防ぐことはできません。cmd
つまり、20バイト長の架空の区切り文字は、キャッチされる前にのバッファを過ぎた18バイトを上書きする可能性があります。
これは、私のコードに壊滅的なバグがあったと言っているのではないことを指摘しておきます(したがって、修正されると、その後も幸せになります)。重要なのは、コードが少し速度を絞るために、危険なバグが簡単に見過ごされたり、「安全でテスト済み」のように見えるものを再利用したときに同じバグが簡単に発生したりするように構成されていることです。コード。これは避けることをお勧めする状況です。
(私は今きれいになります、そして私自身がめったにそうしなかったことを告白します...そしてあまりにもしばしばまだそうしません)。