↓ 下記の質問の結果について、掘り下げて質問させてください。
https://teratail.com/questions/238219
memcpyで、第二引数が第三引数で指定したコピーサイズより小さい場合、
プログラムがクラッシュするケースとしないケースとがあります。
C/C++
1char szString1[1024]; 2char szString2[512]; 3memcpy(szString1, szString2, 1024);
クラッシュしたときのダンプファイルを見ると、
「保護されているメモリに読み取りまたは書き込み操作を行おうとしました。他のメモリが壊れていることが考えられます。」というメッセージが出ていました。
そこで質問なのですが、クラッシュするケースでは保護されているメモリ空間(他によって確保されている空間など)に不正にアクセスしたから、クラッシュしたという理解をしているのですが、合っていますでしょうか?
他の人から、「C++Builder6の場合は、このようなプログラムを書いてもクラッシュしなかった」と言われ、違いを説明できませんでした。
(今回は、Visual Studio 2010 Professionalを使って開発をしています)
そもそも、確保していないメモリサイズを超えたサイズ指定をするようなコードを書くこと自体が間違いなのですが
クラッシュする/しない という現象の違いは、
・動作時のメモリ空間の違い なのか
・コンパイラの違い なのか
考えられるケース、起きる理屈について教えていただけないでしょうか。
気になる質問をクリップする
クリップした質問は、後からいつでもMYページで確認できます。
またクリップした質問に回答があった際、通知やメールを受け取ることができます。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
回答6件
0
メモリレイアウトの自由と OS による管理
メモリのレイアウトがどうなっているかは言語仕様に定めがなく、処理系の裁量によります。 アクセスした箇所が別の用途に使われていたら矛盾を引き起こしてデタラメなことになることもあるでしょうし、偶然にも何も使われていないところかもしれません。
アプリケーションから見える「メモリ空間」に対して物理メモリを割り当てるのは OS の役割です。 きちんとした手順で確保していないメモリにアクセスした場合にはそれを検知できます。 アクセスした場所がそういう場所であればわかりやすく検出してプロセスを即死させることもあります。
様々な理由の組み合わせによって起こるので、何が起こっているか正確に知るにはそのプログラムを観察するしかありません。
有名どころのデバッガとしては
- OllyDbg
- GDB
- Immunity Debugger
といったものがあるので、これらの使い方を学んでみてはどうでしょうか。 実行の様子を観察するのは楽しいですよ。
未定義動作
C や C++ の言語仕様としては確保してないメモリにアクセスにアクセスした場合の挙動は未定義です。 書き込みでも読み込みでもです。
C++ における「未定義動作」というのは非常に影響力が強く、未定義動作としていることをやってしまったらコンパイラはどのような結果を生んでも良いことになっています。 しかも未定義動作をした箇所よりずっと後やずっと前に不可解な動作を引き起こしてもかまいません。 (俗に「タイムトラベル」と呼ばれることもある現象です。)
特に強力な最適化機構を持つコンパイラは未定義がどうなってもかまわないことを積極的に利用して考えられる中で最も速い処理を選ぶことがあります。 単に周辺のコードをまるごと除去することもありますし、容赦なくデタラメな動作に突入することもあります。
超複雑で多数の様々な最適化機構のコンボで意味不明のことが起こるのはよくあることなので C++ では未定義動作に意味のある解釈を試みるのはあまり意味がありません。 前後の状況との組み合わせによっても変わってくるので、考えるだけ無駄です。
投稿2020/01/31 04:43
編集2020/01/31 04:44総合スコア5675
0
C/C++ は良くも悪くも融通が利くので、確保したメモリ領域を超えてアクセスしたとしても、何らかの対処をしてくれません。
超えた場合にどうなるかは環境依存(CPU、OS、コンパイラ、コンパイル時のオプションなど)になるので、常にどうなる、とは言えません。
例えばですが、メモリを16byteだけ確保しようとしたとしても、OSのメモリ管理の都合で実は1KBが割当たってしまうこともあり得ます。そうなると16byteといいながら、1KB使っても文句を言われません。
一方、律儀に16byteだけ確保するようなOSだとしたら、17byte目をアクセスしようとした途端に(OSで)アクセス違反の例外が発生する(そしてクラッシュする)場合も当然ながらあります。
投稿2020/01/31 04:54
総合スコア13703
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
・コンパイラの違い なのか
Visual Studio の場合スタックが破壊または改竄されたことを検知するための実行コードが埋め込まれたりします。
その検知に引っかかるとクラッシュします。
GuardStack (GS)
また、完全にメモリアクセス違反を起こしているとコンパイラが判断した場合、ソースコードを完全に無視して例外をthrowする実行コードが生成されたります。
なぜクラッシュするのか、なぜクラッシュしないのかを確認するにはアセンブリを確認したりデバッガを使って動作を確認する必要があります。
投稿2020/02/08 13:03
編集2020/02/08 13:05総合スコア818
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/02/10 01:08
2020/02/10 01:45
2020/02/10 01:54
2020/02/10 07:36
2020/02/10 13:02
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() の中身はこんな感じです(愚直に書いてみた、ツッコミ無用笑)。
C
1void *memcpy(void *buf1, const void *buf2, size_t n) 2{ 3 unsigned char *d = buf1; 4 unsigned char *s = buf2; 5 unsigned char temp; 6 7 while (n > 0) { 8 temp = *s; // コピー元のメモリを読む 9 *d = temp; // コピー先のメモリに書く 10 ++s, ++d, --n; 11 } 12 return buf1; 13}
「境界」を超えた時点で「temp = *s; // コピー元のメモリを読む」というメモリアクセス命令が
「保護されているメモリに読み取り(中略)操作を行おうとしました・・・」
即ち不正アクセスになり、例外を起こしたのです。
こうしたことを実感するには、各変数、配列などのメモリアドレスを確認するなどして、具体的なメモリ上の配置を調べてみるとよいです。また変数の配置は、コンパイラ(実際はリンカ)の仕様を調べると共に、コンパイル時にリンケージマップ情報を出力させて調べることができます。さらに、荒っぽい話に聞こえるかもしれませんが、わざとバッファーオーバーランアクセスを起こしてみて、どこで例外が起こるか試してみる、なんていう調べ方もできます。
投稿2020/02/04 01:16
編集2020/02/04 13:53総合スコア1382
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
0
そもそも、確保していないメモリサイズを超えたサイズ指定をするようなコードを書くこと自体が間違いなのですが
そのとおりです。範囲を超えたアクセスは未定義の動作になって、何1つ保証されません。もともとメモリに置かれていたデータにすら影響を受けたとしても、文句は言えません。
投稿2020/01/31 04:09
総合スコア145932
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/01/31 04:10
2020/02/03 00:51
あなたの回答
tips
太字
斜体
打ち消し線
見出し
引用テキストの挿入
コードの挿入
リンクの挿入
リストの挿入
番号リストの挿入
表の挿入
水平線の挿入
プレビュー
質問の解決につながる回答をしましょう。 サンプルコードなど、より具体的な説明があると質問者の理解の助けになります。 また、読む側のことを考えた、分かりやすい文章を心がけましょう。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。