パスワードを忘れた? アカウント作成
この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。

C における一時オブジェクトの生存期間」記事へのコメント

  • by Anonymous Coward on 2011年06月19日 20時14分 (#1972743)

    私なりにこの問題点を説明させていただきます。間違っていたらごめんなさい。結論から言うとメモリ領域を本来の意味から逸脱した状態で使っている瞬間があるからだと考えています。

    スタックは下に向かって伸びていくとしますので、スタックポインタより上が使用中、下が未使用と考えてください。

    1.addressee()に入った時にresultはスタック上に確保され初期化される。

    +--------------+
    |return address|←addressee()を呼び出した場所への戻り
    +--------------+
    |<-result      |
    |  a[world\0]  |
    +--------------+←スタックポインタ

    2.addressee()で使った領域を破棄し呼び出し元に戻る。

    +--------------+←スタックポインタ
    |return address|
    +--------------+
    |<-result      |
    |  a[world\0]  |
    +--------------+

    3.addressee()から返された←resultのアドレスからa[]のアドレスを求める。

    4.printf()を呼び出す為にスタックに引数を積む。左側が参考用で右側が実際の様子。

    +--------------+    +--------------+
    |return address|    |  argument 2  |←a[world\0]のアドレス
    +--------------+    +--------------+
    |<-result      |    |  argument 1  |←"Hello, %s!\n"のアドレス
    |  a[world\0]  |    +--------------+←スタックポインタ
    +--------------+

     この後printf()から戻る為のアドレスをスタックに積んでprintf()を実行しますが、a[world\0]の内容は既に破壊されているので期待通りに動きません。

     これはスタックポインタによりa[world\0]の領域が守られていないからですが、この状態はreturn result;の結果としてaddressee()の範囲を抜け}が実行された時から始まっています。なのでprintf()の引数を積まなくとも割り込み等でスタックが使用されるとa[world\0]の領域は壊れてしまいます。「さらに嫌らしい例f()」も同じ効果がありますし数値演算ライブラリを呼び出す計算なんかも同じだと思います。
     処理系の実装に大きく依存しますので、割り込みスタックが別にあったとしても}の後はa[world\0]の領域は本来の意味から逸脱しているので無効と考えるべきでしょう。

    struct X addressee(void) {
        struct X result = { "world" };
        return result;
    }

    同じ着眼点で適合コード [jpcert.or.jp]を見ると

      struct X my_x = addressee();

    の部分で、addressee()で使ったスタックが破棄されてから、struct X my_xに内容がコピーされ終わるまでのわずかな時間は本来の意味から逸脱して使っているので、これも間違いだと思います。

    この間に割り込みが起きてスタックが破壊されたなら、極めて対処のし辛いバグとして苦労する事になると思います。

    • 間違いだと思います。
      addressee を内部的に次のような形に変形し、
          void addressee(struct X *r) {
                struct X result = { "world" };
                *r = result;
          }
      呼び出し側が代入先オブジェクトへのポインタをこっそり渡せば
      きちんとスタック内で完結するコードを生成可能だからです。
      実際 cygwin の gcc (GCC) 4.3.4 20090804 (release) 1 はそのようなコードを生成しました。

      問題は呼び出し側がどう一時変数を用意するかという点にあります。
      例えば次のような関数呼び出しがあったとき、
          void foo(void)
          {
              printf("%s%s", addressee().a, addressee().a);
          }

      gcc拡張の書き方になりますが、次のような形に変換されるかもしれませんし、
          void foo(void)
          {
              /* gcc では ({ 文* }) という書き方で 文を式に変換できます。
                    式の値は内部の最後の式の値です。*/
              printf("%s%s",
                            ({ struct X tmp1; addressee(&tmp1); tmp1.a; }),
                            ({ struct X tmp2; addressee(&tmp2); tmp2.a; }));
          }

      次のように変換されるかもしれません。
          void foo(void)
          {
                struct X tmp1;
                struct X tmp2;
                addressee(&tmp1);
                addressee(&tmp2);
                printf("%s%s", tmp1.a, tmp2.a);
          }

      前者のやり方では printf が呼び出される前に一時変数の寿命が尽きますが、
      後者のやり方ではfoo()の終わりまで一時変数は生きています。
      Cコンパイラはどちらの変換を行ってもかまいません(未定義ですから)。

      適合コードに関しては
          struct X my_x = addressee();

          struct X my_x;
          addressee(&my_x);
      と変換するようなCコンパイラなら割り込みが入っても問題なく実行できます。
      親コメント
      • プログラムを変えちゃったら言語仕様の話にも、コンパイラの実装方法の話にも、根本原因の話にもならないじゃないですか。

        たぶん現行仕様での対応方法は皆判っていて、その上で自動変数の扱いをどの様に拡張したらC++の様に使えて、この様な使い方が誤りでなくなるのかと言う悩みだと思います。

        • いえ、不具合の修正方法の話でも言語仕様の拡張の話でもなく
          「元AC(#1972743)で述べられているスタック操作がおかしい」
          という話をしたつもりでした。
          つまりコンパイラの実装方法の話です。

          「スタックポインタの下は揮発性」という環境下で
          「addressee がスタックポインタの下を指すポインタを返す」というコードを吐くコンパイラは
          間違ってるのではないかと思います。
          親コメント
    • コメント付いているのに気付いてませんでした。

      その説明は誤りだと思います。2. の時点で既に領域が無効だ、という説明かと思いますが、その場合、同様の議論が struct ではなく int に対しても成立し以下のコードも誤りとなってしまいます(3. で持ってくるのはアドレスではなく値になりますが領域自体が無効なら議論は同じです)。

      int addressee(void) {
        int result = 5;
        return result;
      }

      int main(void) {
        printf("Hello, %d!\n", addressee());
        return 0;
      }

      さすがにこのコードが誤りだと関数の戻り値は全て使えないことになってしまいます。確かに result は関数内で確保されていますが値返しで返ってくるため再度スタック上に積まれて呼び出し側でも有効な状態で返ってきます。問題はそのスタック上の値がいつまで有効か、です。C99 規格上では次の副作用完了点まで有効ですので以下のコードは未定義動作を含みません(次の副作用完了点は printf 呼び出しなのでその前に a[0] の評価は完了する)。嫌な例でわざわざ f() 呼び出しが入っているのは別の副作用完了点を入れて問題を起こすため、です。

      int main()
      {
          printf("%c%c!\n", addressee().a[0]);
          return 0;
      }

      親コメント

一つのことを行い、またそれをうまくやるプログラムを書け -- Malcolm Douglas McIlroy

処理中...