質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

89.55%

C++ memcpy コピー元バッファのメモリサイズとクラッシュする現象について

受付中

回答 6

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 509

Taffy

score 16

↓ 下記の質問の結果について、掘り下げて質問させてください。
https://teratail.com/questions/238219

memcpyで、第二引数が第三引数で指定したコピーサイズより小さい場合、
プログラムがクラッシュするケースとしないケースとがあります。

char szString1[1024];
char szString2[512];
memcpy(szString1, szString2, 1024);


クラッシュしたときのダンプファイルを見ると、
「保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。」というメッセージが出ていました。

そこで質問なのですが、クラッシュするケースでは保護されているメモリ空間(他によって確保されている空間など)に不正にアクセスしたから、クラッシュしたという理解をしているのですが、合っていますでしょうか?

他の人から、「C++Builder6の場合は、このようなプログラムを書いてもクラッシュしなかった」と言われ、違いを説明できませんでした。
(今回は、Visual Studio 2010 Professionalを使って開発をしています)
そもそも、確保していないメモリサイズを超えたサイズ指定をするようなコードを書くこと自体が間違いなのですが
クラッシュする/しない という現象の違いは、
・動作時のメモリ空間の違い なのか
・コンパイラの違い なのか
考えられるケース、起きる理屈について教えていただけないでしょうか。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 6

+4

メモリレイアウトの自由と OS による管理

メモリのレイアウトがどうなっているかは言語仕様に定めがなく、処理系の裁量によります。 アクセスした箇所が別の用途に使われていたら矛盾を引き起こしてデタラメなことになることもあるでしょうし、偶然にも何も使われていないところかもしれません。

アプリケーションから見える「メモリ空間」に対して物理メモリを割り当てるのは OS の役割です。 きちんとした手順で確保していないメモリにアクセスした場合にはそれを検知できます。 アクセスした場所がそういう場所であればわかりやすく検出してプロセスを即死させることもあります。

様々な理由の組み合わせによって起こるので、何が起こっているか正確に知るにはそのプログラムを観察するしかありません。

有名どころのデバッガとしては

  • OllyDbg
  • GDB
  • Immunity Debugger

といったものがあるので、これらの使い方を学んでみてはどうでしょうか。 実行の様子を観察するのは楽しいですよ。

未定義動作

C や C++ の言語仕様としては確保してないメモリにアクセスにアクセスした場合の挙動は未定義です。 書き込みでも読み込みでもです。

C++ における「未定義動作」というのは非常に影響力が強く、未定義動作としていることをやってしまったらコンパイラはどのような結果を生んでも良いことになっています。 しかも未定義動作をした箇所よりずっと後やずっと前に不可解な動作を引き起こしてもかまいません。 (俗に「タイムトラベル」と呼ばれることもある現象です。)

特に強力な最適化機構を持つコンパイラは未定義がどうなってもかまわないことを積極的に利用して考えられる中で最も速い処理を選ぶことがあります。 単に周辺のコードをまるごと除去することもありますし、容赦なくデタラメな動作に突入することもあります。

超複雑で多数の様々な最適化機構のコンボで意味不明のことが起こるのはよくあることなので C++ では未定義動作に意味のある解釈を試みるのはあまり意味がありません。 前後の状況との組み合わせによっても変わってくるので、考えるだけ無駄です。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+1

C/C++ は良くも悪くも融通が利くので、確保したメモリ領域を超えてアクセスしたとしても、何らかの対処をしてくれません。
超えた場合にどうなるかは環境依存(CPU、OS、コンパイラ、コンパイル時のオプションなど)になるので、常にどうなる、とは言えません。

例えばですが、メモリを16byteだけ確保しようとしたとしても、OSのメモリ管理の都合で実は1KBが割当たってしまうこともあり得ます。そうなると16byteといいながら、1KB使っても文句を言われません。
一方、律儀に16byteだけ確保するようなOSだとしたら、17byte目をアクセスしようとした途端に(OSで)アクセス違反の例外が発生する(そしてクラッシュする)場合も当然ながらあります。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

変数として確保されたエリア以外のメモリ空間にアクセスしようとしたからエラーとなったってことです
これは、
・CPUの違い
・OSの違い
・動作時のメモリ空間の違い
・コンパイラの違い
のすべてが影響する話ですね

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

そもそも、確保していないメモリサイズを超えたサイズ指定をするようなコードを書くこと自体が間違いなのですが

そのとおりです。範囲を超えたアクセスは未定義の動作になって、何1つ保証されません。もともとメモリに置かれていたデータにすら影響を受けたとしても、文句は言えません。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/01/31 13:10

    > 「C++Builder6の場合は、このようなプログラムを書いてもクラッシュしなかった」

    運が良かったとか悪かったとか、そういうレベルの話です。コンパイルオプションや実行環境を変えれば変わってしまうこともあります。

    キャンセル

  • 2020/02/03 09:51

    私も感覚的には「"たまたま"そのコンパイラではうまく動いていただけなのでは?」と思いうまく説明できなかったのですが、未定義の動作であり、何一つ保証されないものである、という説明ができそうです。どうもありがとうございます。

    キャンセル

0

経験的に言うと(具体的なアドレスなどはともかく)原因も違いも明確です。例外が発生するかしないかの違いは、コピーのソース側となっている配列 char szString2[512]; の配置場所(メモリアドレス)に依ります。もう少し言うと、この512バイトのメモリに続く残り512バイトのメモリ領域の中に、アクセス許可されている(=保護されていない)メモリと、許可されていない(=保護されている)メモリの境界があったという事です。境界が残り512バイトのメモリ領域の中に無ければクラッシュしません。

クラッシュするケースでは保護されているメモリ空間に不正にアクセスしたから、クラッシュした

は正しい理解です。念の為に繰り返しますが、アクセス許可されているメモリとは、保護されていないメモリ、ですよ。

書き込む先のメモリ領域にバッファオーバーランして書き込んだら、他の用途に使っているメモリ(他の変数)を上書きするので、その後、プログラムが異常動作する事は容易に想像できると思います。
一方、ご質問の状況は読み出し側のバッファオーバーランです。通常、コピー元のメモリ領域は、読みだす分にはオーバーランアクセスしたって(メモリを書き変えないのだから)問題を起こすことは無いものです。従って、問題なく動作することは少なくありません。というか、読み出しだけならオーバーランアクセスしても何も問題を起こさないでしょう。

ところがクラッシュするケースがある、それは例外(メモリアクセス例外、OSやCPUによって具体的な呼び名は違うかもしれない)が起こったことを意味します。そうなる理由はただひとつ、残り512バイトのメモリ領域の、ある番地から、アクセスが許可されていない・保護されたメモリだった、ということに尽きます。

プログラムが動作する時、動作に必要なメモリを、OSはアクセス可能なメモリ領域として割当てます。現在のCPUはメモリ管理ユニット、略してMMUを内蔵しており、OSはMMUを制御することで、各プロセスがアクセスできるメモリに制限をかけています。アクセス制限されたメモリ領域をアクセスしようとすれば(プログラムの異常動作であると見做して)例外を発生させ、プログラムを停止するようにしています。それがクラッシュするという事です。

そのアクセスを許可したメモリ領域の中に szString1[1024] も szString2[512]も配置されますが、szString2[512] に続く、残りの512バイトがアクセス可能なメモリである保証はありません。アクセスできたなら、それは運が良かったわけです。szString2[512]をメモリのどこに配置するか、なぜ境界の近くに配置されたのか、はコンパイラと、OSの、両方の事情の合わせ技のようなもので決まります。お使いのOSとコンパイラ次第なので具体的な説明はできかねますが。

memcpy() の中身はこんな感じです(愚直に書いてみた、ツッコミ無用笑)。

void *memcpy(void *buf1, const void *buf2, size_t n)
{
    unsigned char *d = buf1;
    unsigned char *s = buf2;
    unsigned char temp;

    while (n > 0) {
        temp = *s;       // コピー元のメモリを読む
        *d = temp;       // コピー先のメモリに書く
        ++s, ++d, --n;
    }
    return buf1;
}


「境界」を超えた時点で「temp = *s; // コピー元のメモリを読む」というメモリアクセス命令が

「保護されているメモリに読み取り(中略)操作を行おうとしました・・・」

即ち不正アクセスになり、例外を起こしたのです。

こうしたことを実感するには、各変数、配列などのメモリアドレスを確認するなどして、具体的なメモリ上の配置を調べてみるとよいです。また変数の配置は、コンパイラ(実際はリンカ)の仕様を調べると共に、コンパイル時にリンケージマップ情報を出力させて調べることができます。さらに、荒っぽい話に聞こえるかもしれませんが、わざとバッファーオーバーランアクセスを起こしてみて、どこで例外が起こるか試してみる、なんていう調べ方もできます。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

0

・コンパイラの違い なのか

Visual Studio の場合スタックが破壊または改竄されたことを検知するための実行コードが埋め込まれたりします。
その検知に引っかかるとクラッシュします。
GuardStack (GS)

また、完全にメモリアクセス違反を起こしているとコンパイラが判断した場合、ソースコードを完全に無視して例外をthrowする実行コードが生成されたります。

なぜクラッシュするのか、なぜクラッシュしないのかを確認するにはアセンブリを確認したりデバッガを使って動作を確認する必要があります。

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/02/10 10:54

    > read時のアクセス違反ならGSじゃない
    > 548バイト以降はメモリ保護がかかっている部分にアクセス

    なら、私の見立て通りなのでは。

    キャンセル

  • 2020/02/10 16:36

    DLLのGSスイッチを切替えて動
    作させてみました。
    有効にしたとき(デフォルト)…発生する
    無効にしたとき…発生しない

    このことから、GSが有効となっていることが関係しているのではないかと思いましたが
    Read時のアクセス違反だと、GS関係がないのでしょうか?

    GSスイッチの切り替えについては、以下のサイトを参考にして実施しました。
    https://docs.microsoft.com/en-us/cpp/build/reference/gs-buffer-security-check?view=vs-2019

    キャンセル

  • 2020/02/10 22:02

    /GSはスタックの破壊(Write)を検知するものです。
    > 当該の処理はDLL側のプロジェクトに書かれており、
    Visual Studio 2010で再現可能な最小のソースコードとプロジェクト(コンパイルの設定)もしくはアセンブリなどを提示してもらわないと確認不能です。
    https://teratail.com/questions/238219
    こちらのソースコードを見てみましたがヒープ上のメモリにアクセスしているので、こちらで提示されているソースコードとは状況が異なります。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 89.55%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る