0

argvシステムコールに使用する単一の文字列に解析して戻そうとしています。printf文字列は(ターミネータがなくても)呼び出しを通じて正常に表示され\0ますが、システムへのパラメータとして使用すると、あらゆる種類の未定義の動作が作成されます。

文字列が正しく終了していることを確認するにはどうすればよいですか?char[][]構文解析を行うためのより良い、より信頼性の高い方法はありchar[]ますか?

#include <stdio.h>
int main(int argc,char *argv[]){

char cmd[255]="tcc\\tcc.exe ";
char**ptr=argv+1;
while(*ptr){
   strcat(cmd,*ptr);
   ++ptr;
   if(*ptr)cmd[strlen(cmd)]=' ';
}


printf("cmd: ***%s***\n",cmd);
system(cmd);
}

このコードの別の欠陥を発見して修正しました。システムコマンドには、ファイルパスに(エスケープされた)バックスラッシュが必要です。

4

2 に答える 2

3

この指示:

   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バイトを上書きする可能性があります。

これは、私のコードに壊滅的なバグがあったと言っているのではないことを指摘しておきます(したがって、修正されると、その後も幸せになります)。重要なのは、コードが少し速度を絞るために、危険なバグが簡単に見過ごされたり、「安全でテスト済み」のように見えるものを再利用したときに同じバグが簡単に発生したりするように構成されていることです。コード。これは避けることをお勧めする状況です。

(私は今きれいになります、そして私自身がめったにそうしなかったことを告白します...そしてあまりにもしばしばまだそうしません)。

于 2013-03-02T16:44:48.120 に答える
1

これは必要以上に複雑になる可能性がありますが、バッファオーバーフローを回避し、バッファが小さすぎる場合にも終了します。バッファに十分なスペースがない場合にループを続行すると、省略された場合でも、文字列に追加されるよりも短いargv[N]末尾の文字列(argv[N+1]など)が発生する可能性があることに注意してください...argv[N]argv[N]

を使用していることに注意してください。memcpyその時点で、私はすでにどのくらいの長さであるかを知っているからargv[i]です。

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;

    for (int i = 1; i < argc; ++i) {
        size_t len = strlen(argv[i]);
        if (s + len >= end) {
            fprintf(stderr, "Buffer overrun!\n");
            exit(1);
        }
        *s++ = ' ';
        memcpy(s, argv[i], len);
        s += len;
    }
    *s = '\0';
    printf("%s\n", cmd);
    return 0;
}
于 2013-03-02T17:44:35.993 に答える