http://cowboyprogramming.com/2008/09/09/debugging-memory-corruption-in-game-development/
以下エキサイト先生のエセ翻訳。
#9/24 訳文整理
#9/25 訳文整理パート2
#9/26 訳文整理パート3、仕事中にやるにしては長すぎるだろjk
#9/30 パート4、あまり時間とれず
#10/1 パート5
#10/2 パート6
#10/6 パート7
ゲーム開発における、メモリ破壊の検出とデバッグ方法
By Mick West
概要:
メモリ破壊の兆候
メモリ破壊の調査
メモリダンプの読み方
メモリ破壊の兆候と効果
定義:
メモリエラーは、記憶領域かメモリ領域の内容をびっくり変化させちゃいます。 |
メモリ破壊は、コーダーが巻き起こす究極に最強で最悪のエラーです。
メモリ破壊の兆候は、(まったく気付かないくらい些細に壊れている場合を除き)大きなクラッシュを基軸としていろいろな『変化』を起こす可能性があります。
メモリ破壊の原因はまったくもって様々であり、さらにメモリ破壊自体を含んでいます。
(メモリ破壊がメモリ破壊を呼ぶ・・・・・・終わりが無いのが終わり・・・・・・・・)
i. メモリ破壊の兆候
「メモリ破壊がやらかしたこと」を全部明白にできるのであれば、すべての兆候を追うのは余分なことに思えます。
しかしながら、メモリ破壊が起こす『異なった』兆候は、時々原因が違うものを暗示しています。
また時々、兆候の種類によっては(メモリ破壊についての)有益な手がかりを集めることができます。
(兆候は原因に近い場所を教えてくれるかもしれませんよ?)
Case.1 クラッシュ
クラッシュは、いろんなタイプのヤバげなバグを引き起こします(第23章を参照)※1
んでもって、メモリ破壊はそのクラッシュを引き起こす場合があります。
#HAHAHA、メモリ破壊に何度苦しめられたことか!
どんなタイプのメモリ破壊が起こるのか、のいずれの場合に際しても
「ゲームがブッ壊れた」という情報・状況は有益な手がかりとすることができます。
これらの手がかりは、どこでブッ壊れていて、どこらへんを探れば解決できるのかを示してくれます。
アドレスエラー
アドレスエラーとは、ポインタ(出た!)が間違ったアドレスを示している(ように変更された場合の)ことです。
でもこれは、以下のような場合のアドレスかもしれません。
・そのアドレスは、『データなし』≒『NULL』
・またはそのアドレスは、『参照外のメモリ領域※2』か、『保護されたメモリ領域』
アドレスエラーはかなり有益な情報です。
アドレスエラーに遭遇したとき、プログラムは停止し、デバッガが起動します。
そこで使用されているポインタ変数のアドレスと、崩壊したコンテンツを特定するのはかなり簡単なことです。
無限ループ
不正なデータは無限ループを作る可能性があります。
例えばチェインリストを全検査するコードを想像してください。
そう、山手線のような構造になったチェインリストです。
メモリはループされたリスト構造よってぶっ壊されるかもしれません。※3(スタック・オーバーフローとかで)
書かれたコードは、NULLが何らかのポイントにある状態でリストが終わると予想するので
それはいつまでも、ぐるぐると回り続けることでしょう。
この振舞いは、ポインタの代わりにインデックスを使用するリストのほうが多そうです。
でも、この問題はどっち使った場合も起きる可能性があります。
そのケースが発生する場合について考えてみましょうか。
まず、リストが間違ったポインタを手に入れるところから始まります。
そのポインタが、リストのどこか前のほうを指している場合───
そう、たったいま、リストは終端の無い『円形状』に変化しました。
このケースが、なんらかのメモリトラブルによって起きたエラーであるという例は超絶ありえません。
(もしそうなら、すっげえ運が悪いということです)
または、何らかの関係ないコードが、た・ま・た・ま正しいポインタを、た・ま・た・ま『ちょっとだけ間違ったポインタ』に上書きするって言うのも
あんまり考えられません。(悪意あるコードであったとしても、狙ってやるのは難しいです)
したがって、リストコード自体になんらかの原因があると思われる訳です。
命令違反
命令違反は、メモリ破壊でよく見られるケースが多いのです。
[スタック・エラー / Stack Corruption]
スタック領域が何らかの方法でぶっ壊ちゃってた場合
プログラムは間違ってリターンされたアドレス(にあるコード)を実行しちゃうかもしれないです。
(それはつまり不正コードってやつだ)
これはバッファオーバーランによって引き起こされる場合が最も多く
ハッカーがコードをのっとるときによく利用している現象です。
[ジャンプテーブル・エラー / Jump Table Corruption]
仮想関数テーブル(または、飛び先のどんな種類のテーブルも)が崩壊する場合
PCはソコに不正な命令を指し示すことができます。
[コード・エラー / Code Corruption]
コード自体は、一部のぶっ壊れたコードによって全体が壊れることがあります。
このタイプのエラーは、腐敗しているコードが頻繁に実行されないかどうかの検出が
非常に困難である場合があります。
[スタックの上書き / Stack Overwriting Code]
これは、特殊なエラーに属するものです。
暴走した再帰処理などは、スタックが処理を実行しているルーチンを上書きするまで止まりません。
この現象は、メモリダンプなどのヘックスウインドウ(デバッガ上)でうまく現れます。
[ファンクション・ポインタ / Function Pointers]
ファンクション・ポインタは、データ構造体内によく定義され、通常の変数のように使い回されるので
つまり、それは通常の変数と同じようにぶっ壊れることがあるってことです。
これは結局、不正なコードを実行するプログラムに通じる可能性があるわけです。
Case.2 想定外の値
#ホリエもコレに騙された訳だな。
それはさておき
変数はなんでもそうですが、一定の範囲内で値を持っています。
変数がなんかおかしい値を含んでいた場合
「メモリ破壊のせいかもしれない」と
あなたは不意に気付くはずです。
むやみやたらに珍しい値には、たまーに派手な効果を引き起こすことがあります。
プレイヤが世界の果てへワープしたり、またはモデルが無限にスケーリングされている状態などです。
こやつらは、たいした破壊を起しません
例えば、単にカウンタをゼロにリセットするか、またはちょっとばかり減らしたりするくらいです。
特にめぼしい効果を生まないかもしれないので、このタイプのエラーは捜し出すのが難しい場合があります。
ここらへんを調査するテスト部門は非常に貴重です。
テスターがこのような小さい矛盾に気付くことができると
あなたは遥かに高い確率をもって、初期の段階で潜伏している有害なバグを捕らえるコトができるでしょう。
メモリ破壊の位置はよくコロコロと場所を変えるので、問題はしぎらく見発見のまま進行することがあります。
これは「既存のコードがしっかりしたモノである」と、間違った印象を与えるかもしれません。
そのときに、新しくコードかデータを加えると、バグは現れるかもしれません。
そうなった場合、あなたは「新しく追加したデータがバグを引き起こしたんだ!この○○○○!」と
激しい思い違いを起こしてしまうかもしれません。
───事実上は、新しいコードがメモリを再構成したため、(たまたま)バグを明らかにする構成になったというわけですが。
Case.3 グラフィクスよるトラブル
メモリはグラフィカルなデータを含んでいるコトが多く
もしメモリが壊れ始めたとき、プログラムはいくつかのグラフィックスの「ゆがみ」を私たちに見せているかもしれません。
これらがどのようなコトをするかってのは、グラフィックスの本質とエラーの本質によるでしょう。
テクスチャ
1ピクセルの色の変化、または非常に短い行または列のピクセルの変化は
ポインタ変数が間違った値(恐らく以前のメモリエラーの結果)を取得したことを示しています。
テクスチャが大きく乱れた場合は、不正なポインタか、ある種のバッファオーバーランのどちらかです。
また、基本的にはそんなに壊れていないのですが、垂直または対角線のしましま模様がテクスチャに出てくる場合は
間違ったアドレスを突っ込まれたか、または領域をあふれた何かしらの配列が原因でしょう。
別のテクスチャに押しつぶされたか、変色したバージョンに類似しているテクスチャのエラーは
何か別の、異なったサイズかビット深度を持つテクスチャで上書きした場合でしょう。
エラーが静的である(変わらない)場合、それはポインタが一度だけ誤用された───
つまり(これから始まるメモリ破壊連鎖の)1回目の出来事であることがわかります。
『ゲームの途中でエラーが発生しました』
(しかしまだゲームは動いています)
この場合、あなたはその出来事の引き金となった原因を捜し出す必要があります。
テスターは、映像的に壊れている部分と、それにつながる事情を(ログとして残すために)記す必要があります。
ゲームを映像に記録することは、この場合に非常に役に立ちます。
壊れたナニカがアニメーションしている、壊れたナニカが点滅している、対になる領域が点滅している場合
(メモリ破壊の)破壊活動が今現在、活発に進行中です。
ゲームがその状態で(奇跡的にもまだ)動いているなら、それはそれでデバッグするのがより簡単になります。
メッシュ
まあ普通は、壊れたメッシュは元のモデルからかけ離れるほど酷く、しっちゃかめっちゃかに頂点を置き換えられています。
エラーが小さいなら(単語かナニカ)、あなたは、とある頂点が面白い位置に置き換えられるのをただ見ることができて
モデルとしてスクリーンに表示されているそいつは、むやみやたらに揺れ動きまくる薄い三角形か線に見えるでしょう。
多量のメッシュが壊れているとき、それはまるで「爆発」のような結果をあなたにプレゼントしてくれます。
無作為に点滅し、揺れ動きまわる三角形がスクリーン全体を覆っているのですから・・・・・・・・・
#1回なったことあるけどスペックマシンでもFPSが超落ちるよ。
スケルトンとアニメーション
基本的なスケルトンデータ、または関連アニメーションのエラーは
まだいくらかは(認識可能なほどに)見えているモデルとして表示されますが
身体の各パーツが、ものすごい異常な位置に置き換えられている状態でしょう。
ぶっ壊されたアニメーションはむやみやたらに動き回り、おかしく跳び回る身体のパーツ部分を見ることが出来るでしょう。
(エラーの兆候の正確な顕現は、アニメーションを格納するのに使用される方法によります。)
#スマブラXのボーンをhackして置き換えた動画をみると判りやすいと思うんだ。
ii. エラー調査
メモリ破壊が起こっていると疑うなら
まずはじめに、どのような現象で、どのような種類のエラーかを断定することにしましょう。
Research 1. まじで壊れてるの?
メモリの値が、かなり珍しい値のようでも
その値は必ずしも「メモリ上にあるコードから生成されている」ことはないカモということです。
ヘンな値は、単にコーディング・ミス(論理エラー)の結果かもしれません。
それか、他のどっかからまったく正しい値としてコピーされたのかもしれません。
または、間違ったデータを含む計算結果───もしかすると恐らく、そのデータは既にぶっ壊れていたもの───かもしれません。
これを断定するためには、
値を書き換えている場所、つまり「ここら辺が間違っているんだろうな」と思う場所を特定し
そこに監視コードを書いて、値が間違っているのかどうかをチェックする必要があるでしょう。
理想をいえば、値を書き換えている可能性がある場所全てに監視コードを書き
値の書き換えを行っている範囲をチェックするとよいでしょう
(ただし、チェックしている範囲外に「不正な」値があるのを確実にしてください)
Research 2. この記憶域を所有しているのは誰だァー!?
※4
通常何らかのコードが、そのコードが使用するべきでないメモリの領域を使用しているときに
メモリ破壊が発生します。
そして、ぶっ壊れたメモリは何らかのコードの問題を引き起こします。
これを起こす、2つの前提条件があります:
法的に守られた(正常な他のコードの)領域を破壊し───その不正な領域を使用することです。
コードAが、メモリ領域A(m)を使用していると考えてみてください。
ここで、別のコード"B"が、たまたまメモリ領域A(m)のポインタを持ってしまったとしましょう。
この際に、コードBが何回かデータを書きかえたなら、コードBはメモリA(m)を崩壊させています。
これが、メモリ破壊の正規形です。
今度は、コードAがメモリ領域A(m)を法的に使用している場合を考えてください。
また、コードBはいくらか不法に(またはオーバーラップされた)メモリ領域A(m)の領域を使用しています。
コードBは正しく働くように見えますが、コードAは法的なアップデートをA(m)にします───コードBが『バグ』であると判断して。
この時点では、コードAがメモリB(m)を破壊しているように見えます。
しかしながら、バグはここ『コードB』.にあります。
もしこうなったなら、あなたは『このコードAがメモリをぶっ壊してるのね! やなやつ!』と
ミスリードしてしまうかもしれません。
実際に、誰が『ぶっ壊れたメモリ領域』を所有しているか、を断定するのが重要です。
・「法的な」使用は本当に法的ですか?
・コードBが本当にものメモリ領域を所有しているコトを証明できますか?
コードBが「本当はそのメモリ領域を使っているわけじゃない」とすぐに断定できるなら
コードAの追跡には無関係です。
(これで、結構な時間を節約することができます)
#具体的に言うと4時間から1日、多くて3日くらい。
Pattern A. いつも同じ場所で起こる、再現性100%のヤツ
エラーが同じ場所で、同じ条件のもとで発生し、しかも症状が一貫している場合
(比較的に言えば)あなたは運が良いといえます。
この場合のデバッグは、どうにかエラーの場所を確認して、原因を捜し出すだけの問題です。
エラーが同じ条件のもとで起きるので、すぐにバグは取れるか
または、トラップを仕掛ければバグが起きる可能性を絞り込めるはずです。
Pattern B. 同じ場所で起こるけど、たまーにしか出てこないヤツ
エラーが同じ場所で起こるのですが、たまにしか発生しないのなら
それだけで、エラーを捜し出すのがより難しくなります。
エラーがいつ起こるかを知らないで原因を捜し出す場合、原因を絞ることが難しくなるので
エラーの本質に関してまだ一般的な観測に頼るほかありません。
Pattern C. たまーに出てくるうえ、いろいろな場所で起こる
エラーがいろいろに場所、および予測できない時に発生するなら
デバッグオプションは、メモリ破壊が起こった後にエラーに関して観測をするレベルまで制限されます。
Research 3. エラーが起きる位置を特定しないといけませんね
メモリ破壊が、間違ったポインタによるアドレスエラーなどのバグの近因であるなら
単にバグがおきた時点で、どんなアドレスがアクセスされていたかを見ることで
あなたはすぐに、エラーの影響(効果)を特定できるかもしれません。
メモリ破壊が中間的な原因であるなら
バグの近因、および根本的原因と兆候の間にある、すべての中間的原因についてを分析している途中で
あなたは問題のアドレスを捜し出すでしょう。
ハードウェア・ブレークポイント
目的のプラットホームに、ある種のブレークポイント(アクセスの中断が可能なモノ)があるなら、
メモリ破壊をデバッグするとき、まずはこれをわかったアドレス(おそらくはエラーが起きたアドレス)に仕掛けてみてください。
あと、デバッガにはメモリ領域が変化したときにブレークポイントを実行するように設定しておいてください。
そして問題が起こったときにはあわせて、コードの実行している部分も見てください。
ぶっ壊れている場所が比較的静的なデータの場合、このテクニックは非常に効果的です。
しかしながら、その場所に1フレームに何百回も更新する、何らかのダイナミックな変数を含んでいる場合ですと
その何百の中から『たった一つの原因とおもわれるモノ』を見つけるのは非常に難しいワケです。
その場合、その超大範囲にあるブレークポイントたちは
『そのデータが有効であるかどうか』といった、条件をつけることである程度絞り込むことが出来るでしょう。
たまに、メモリは有効範囲内の値を書き込んだにもかかわらず、壊れることがあります。
しかし、それは間違っています。
その場合は、もうちょっと条件を絞るといいでしょう。
◆繰り返しコードを実行しましょう。
そして、ブレークポイントに引っかかったならば、呼ばれたスタックを一個一個追いかけ
あなたは、それがその場所に正しく(法的に)書くことができるコードであるかどうかを確認しましょう。
間違っているとわかる何かを見つけるまで、スタックを追い続けます。
※6
◆問題のおきた「保護されたメモリ領域(法的な場所)」がわかっていて、それが比較的限られる場合は
まずそれらを更新するために、最初に書き込んでいる部分をいくつか別の位置に分割します。
エラーが起きたとき、それがそれ別々に分けたデータに影響していないなら
ブレークポイントを、保存された値にマッチする条件に更新すると良いでしょう。
◆コードを追っかけてみましょう。
関数にステップし、壊れているものを1個(でも)見つけたら
次回にそこを通る際は、その関数を注意してみるとよいでしよう。
◆手動メソッド(※7):
ステートメントを散らし、(その)アドレスを特定しましょう。
そして、そのステートメントがアドレスを更新しているのをチェックしましょう。(位置を報告すればよい)
◆メモリマネージャチェック:
メモリマネージャに、エラーの位置を渡してみてください。
それがNULLでないなら、そのブロックすべての情報を残しておいてください。
iii. メモリダンプを読みましょう
このメモリ領域は腐っている!
#つまり、すべからく是正されねばならんのですね○ルパラッツォ様!
#壊れている、が正しい訳と思いますがまあ、Exciteサマが腐っているって言ってるので。
コードの何bit分が不正を引き起こしているのか、あなたはすぐに見つけられないと仮定します。
あなたはエラーの本質を調べることで、そのコードが「何であるか」を大いに学ぶことができます。
あなたは一度、クサっている位置を特定して
次にデバッガで、その部分のヘックス(メモリ)・ダンプを見ます。
(デバッガが利用出来ないか機能が無いなら、印刷かなんかしてください)
ヘックスダンプは、このように見えます。
0x00322B90 0x00322B98 0x00322BA0 0x00322BA8 0x00322BB0 |
fd fd fd fd ab ab ab ab ab ab ab ab ee fe ee fe 00 00 00 00 00 00 00 00 12 00 0d 00 22 07 18 00 48 2b 32 00 40 2c 32 00 |
ýýýý«««« ««««îþîþ …….. …."… H+2.@,2. |
メモリアドレスが左にあって
メモリの中身(この場合は1行が8バイト)が真ん中
そして一番右の部分は、メモリの内容をASCII文字として記載したものです。
ビットエラー(single)
数コードで、1ビットだけをはじき出すでしょう。
最もありそうな候補はビットフラグなどです。
バイトエラー(single)
1バイトだけが変更されたなら、それは分野をかなり限定できます。
不正な値が0か1であるなら、それはおそらくバイトフラグです。
32ビットワード・エラー(single)
32ビットワードは、データを格納するうえで最も速くて最も便利な方法です。
これは、浮動小数点やポインタなどのデータ型を唯一使用できるモノです。
その32ビットの内容は、「値をそこに挿入したコードに関する何か」をあなたに教えてくれるとでしょう。
32ビットの値が崩壊しているのが分かっているなら
あなたはメモリ上のこれを『32ビットのただ一つの単語』であるとみなすべきです。
むしろ4バイトのシーケンスとして。
こうすれば、エンディアンへのどんな混乱も関係なく、データタイプを認識するのがはるかに簡単になります。
バイト・ストリームを、特定の型のデータであると解析できるのは、役に立つ技術といえます。
また、データは異なった型の、ほかのデータ型と混ざるかもしれません。
(クラスなどを使用している場合)
以下に、それぞれの値の例を示しておきましょう。
・32ビットの整数
・および4バイトのリトルエンディアン形式
・ビッグエンディアンとどっちが認識しにくくいか?
・単語がどのくらいの感覚で点在するのか
[ゼロ / ZERO]
例:
00000000 or 00 00 00 00 |
ゼロはわかりやすい表現です。
まず、ゼロが出たときに『そこに手がかりがある』とは思わないコトでしょう。
なんらかのコードの一部が、なんらかの理由をもって、ゼロを代入することはあるでしょう。
それが何故おきたか、についての手がかりくらいは得ることが出来るかもしれません。
ゼロ とは
[NULL]
恐らくは、間違ったコードがポインタを綺麗にしています。
プログラマの中には、ポインタが指していたものなら全て削除したあと
そのメンバ変数だったすべてのポインタもきれいにすることを心がける人もいます。
(ぶら下がりポインタを防ぐための妥当な習慣ではある)
しかし、それが誤作動することもあります。
#あるある
[ゼロ / Zero]
整数の0、または浮動小数点(0.0f)のことです。
どこかに一行ほど、0を入れているコードがありませんか?
[偽の値 / FALSE]
たぶんコードは、フラグとしてコレを扱い
単にそれがFALSE(偽)として設定されているだけの状態です。
[enumにおける最初の値]
おそらくは、状態遷移か何かに使うなタイプ・フィールドでしょう。
その容疑者(コード)はどんな種類の列挙型を持っていますか?
初期値はどんな役割を意味していますか?
どうして、コードは最初の値をセットしたのでしょうか?
[1・いち・イチ / ONE]
例
00000001 or 01 00 00 00 |
1はもっと判りやすいでしょう。
ゼロより一般的ではありませんし
ゼロよりはもっといろいろな手がかりを教えてくれるコトでしょう。
イチ とは
[真の値 / TRUE]
おそらく、それはフラグです。
TRUEであれば、何を設定できますか?
[整数 / An integer]
つまり、それは浮動小数点ではありません。
あなたはfloat型を格納するコードを無視できます。
[ポインタではない何か]
まあたぶん、エラーを引き起こしたコードは
この値をポインタであるとは思っていません。
[enumの最初の値]
どんな小さい数であっても、それが列挙された値であれば可能です。
ことによると形式数であるかもしれません。
[浮動小数点 / FLOAT]
例:
3F800000 or 00 00 80 3F ※8 |
多くの浮動小数点は、容易に認識可能な形式で出力されています。
非常に一般的な浮動小数点値は、3F800000を基準として考えられます。
(32ビットの浮動小数点で1.0をあらわしています)
この値がどのように変化するかは、リスト24を参照してください。
リスト24↓
Float 0.00000000 0.50000000 1.00000000 -1.0000000 2.00000000 100.000000 0.33333334 3.14159274 |
Int 00000000 3F000000 3F800000 BF800000 40000000 42C80000 3EAAAAAB 40490FDB |
ナベアツ的な数字に注意してください。(最上位ビットが3から始まる=1より小さい数のことです)
浮動小数点は
・最初のビットに符号
・次の8ビットに指数部
・そして以下の23ビットが分数部分で構成されています。
ここで、いくつか同じ指数部を持つ数があることに気付けると思います。
-1.0~1.0までが、ゲーム中に見られる非常に一般的な浮動小数点値の範囲です。
これらの数は
・ユニットベクトル
・変換行列
・UV座標
・スケーリング値
などに手広く使用されています。
通常、この範囲の数は最上位ビットが 3(正の数) または B(負数) から始まります。
値が浮動小数点であると疑ってかかる場合
それが定数(オリジナルの値・変更のない値)、であるか、計算された値かを知ることが出来ます。
リストの値を見てください。
1.0、2.0、0.5、100.0にあたる値はすべて末尾にゼロを持っています。
3.3333334については、ダンプのAAAAAAという(計算で出たとは思えないような)値がある点を評価します。
#つまりメモリダンプに数字がキレイにならんでたら、それはオリジナルの値である可能性が高いわけだ。
それとは対照的に、無理数である3.14159274は
ダンプ上に、16進数ナンバが無作為に並んでいるように見えます。
16進で表現された浮動小数点の情報の精度が、いかに不確かであるか見ることが出来ます。
いくつかの計算に使う浮動小数点は、無作為の十六進ナンバをもっと持っていそうです。
そこで、機能拡張した部分が「(問題の)コードらしいコード」を教えてくれることでしょう。 ※9
p->m_speed = sqrtf(p->m_speed*p_m_speed – 2.0 * g * h); |
(または初期化部分で以下のようになっていました)
p->m_speed = 2.5f; |
[ちいさい整数 / Small Integers]
わずかな整数(0~10000くらいまで)は、通常カウンタか列挙型です。
均等に(規則的に?)値が増減していれば、それはカウンタです。
いくつか一定の値がコロコロと切り替わっているのなら、それは十中八九状態をあらわす変数です。
このちいさな整数どもは、不正発生時点でゲームの何を表すのでしょう?
可能性の例:
・スコア(Score)
・ヒットポイント(Health)
・残機(Lives)
・レベル(Level number)
・武器の種別等(Weapon number)
・ボタン投下状態フラグ(Button Pressed)
ゲーム中の値とエラーの値の間に、なんらかの相関関係を見つけられるよう努力してください。
[でっかい整数 / Large Integers]
変数の値が大きくなるにつれ、それの用途は減っていきます。
たとえば、10万項目以上のグループを管理する場合はあまり考えられません。
巨大な整数があったなら、それが何を数え、表しているのかを考えるべきです。
その時、それが整数値ではなく
じつはポインタ、またはコードアドレスではないかどうか、考えてください。
[負の整数 / Negative Integer]
例:
FFFFF3A2 of A2 F3 FF FF |
一般的に、整数はものを数えるために使用されます。
負の整数があったとしたら、それは使用されうる用途の範囲をかなり限定します。
なんらかのコードで(追加でフラグを用意することなく)負数をフラグに含めることで
コードをスイッチングすることが出来ます。
負数はエラーコードとしてよく使用されます。
いくつかの機能はポインタをパラメータとみなして
次に、(エラーのあった)ポインタの位置を示したエラーコードを返します。 ※10
ポインタが間違っているなら、それは負数があるメモリ破壊に通じることでしょう。
[マジックナンバー / Magic hex Numbers]
例
DEADBEEF or EF BE AD DE |
デバッグ中に見られる「まほうの☆16進数」は
デバッガで目立つように、プログラマによって意図的に選ばれた16進数です。
また、そのマジックナンバーは、たまたま間違ったとかそういうレベルでなく、間違いとはっきり判る値、
───つまり、エラーをプログラマに警告できるような値が選ばれています。
最も一般的な使い方は
初期化時(メモリを確保(アロケート)する際と解放(フリー)する際の両方)に、この値を割り当てるやり方です。
これは、ブロックをデバッガ(メモリウィンドウ)で視覚化出来るようにする効果と
(このさい、メモリブロックはマジックナンバーで埋め尽くされ、プログラマの注意を引くことが出来るでしょう)
また、メモリが正常に初期化される前に使用されていないか
または、メモリが解放された後に使用され続けていないかなどを検知する効果があります。
一般的なマジックナンバーには、以下のようなものがあります。
CCCCCCCC #VC++ 6.0? CDCDCDCD #.NET C++ DEADBEEF #屍肉(音) DEADDEAD #屍屍(音) DDDDDDDD #? FDFDFDFD #? |
マジックナンバーの値は、各プラットホームで異なります。
開発者は自分で考えたマジックナンバーを使用することもよくあります。
そのナンバーは、声に出して読めるものを好む傾向があります、DEADBEEFなどのように。
[マジック・アスキー / Magic ASCII]
例:
474E5089 or 89 50 4E 47 or ‰PNG |
アセットファイル※11 にはファイルの種類を示す4バイト(かそれくらい)のASCIIストリングがついていて
人間にやさしい(ファイルタイプを特定しやすい)ように設計されています。
これが一語だけ間違っているというのはそうそそうありません。
ただ、コレが間違っているかどうかチェックするために、メモリウィンドウでASCIIコラムの中を見る価値はあります。
もしもの文字列を認識できたなら、間違っている部分を希望を持って指摘できることでしょう。
[ポインタ / POINTER]
例
00434150 or 50 41 43 00 |
32ビットのポインタは、4ギガバイトものアドレス範囲を表現・利用可能です。
プログラムは通常、そのうちのいくつかを占領します。
したがって、ポインタは認識可能な範囲のものが良く使われます。
Win32では、アドレス00400000(4MB目が仮想アドレス空間の開始位置)から始まります。
静的データのポインタは、よく004(プログラムを書くうち005、006と大きくなっていく)から始まります。
PS2では、00100000(1MB)からが実行可能な位置です。
よってポインタは001、002などから始まるでしょう。
ファンクションポインタは不正データになりそうもないので
もしこのようなポインタを見つけたら、それはおそらくいくつかの静的なデータのポインタです。
静的なデータのうち、最も一般的なタイプのポインタはストリングへのポインタです。
不正データにポインタを持っているように見えるなら、それを追ってみてください。
そして、それが認識可能なストリングかどうかを確かめてください。
プラットホームによっては、ポインタは種別に整列された単語のように見える傾向があるかもしれません。
PS2では、コード化するポインタか、それ以外(任意のサイズの単語データ)で分けられていました。 (? ※12
PCはバイトレベルにおけるすべてのデータの参照を許します。
[なにかの数 / Random Numbers]
例
9D29F113 or 13 F1 29 9D |
ゲームで確保されたメモリに目を通すとき
あなたは、無作為に見えるとても小さなデータを見つけるでしょう。
それはだいたいゼロがいっぱいあって
データがより密接にパックされたところは、バイトかパターンが(メモリを? 画面を?)支配しています。
それで、無作為に見えるなんらかの数を見つけたとき
その数には、必ず何か意味があるはずです。
以下に、いくつかの可能性を列挙します。
[浮動小数点 / A floating point number]
既述のとおり、いくつか有効数字幅がある浮動小数点はちょっと無作為に見えるでしょう。
パイ定数(3.141592654)は40490FDB(無作為に見える)として出て来ます。
[チェックサム / A checksum]
コードでCRC32などのチェックサムを使用するなら
ファイル特定などの何らかの理由で
これははぐれたビットデータかもしれません。
できれば、どんなストリングがこのチェックサムを発生させるかを見てみてください。 ※13
[圧縮されたデータ / Compressed data]
よく圧縮されたデータはランダムな数字列に見えるはずです。
最後の一文字だけ壊れているというのはありそうもないですが、しかしもしかしたら・・・・・・。
[テキスト / Text]
テキストは一見無作為に見えますが、バイトがほとんど0x30~0x7Fの範囲内であれば
それがストリングの断片である可能性はあります。
メモリウィンドウでどのようなASCII文字列かを確認してください。
ブロック・エラー
ブロック・エラーはメモリ上の単語グループが多少改悪されてしまうコトです。
ブロックは何処までもでかくなります。
しかし、ブロックといえば私たちは暗黙的に4~1024バイトまでのサイズのものを挿しています。
ブロックの不正データは一語で見つけられた不正データのタイプのどんな組み合わせも含むかもしれません
ですが、以前に議論したように
不正を妨げるために、特定できるいくつかの状況があります。
[部分的な不正 / Partial corruption]
カバーされたメモリのブロックのデータ完全に壊れてなく
いくつか、ないし多くのバイト列が変わっただけなら
これは私たちが紛失したデータ構造(構造かクラス)のポインタを処理した、という良い指令です。
最もありそうな事例はぶら下がりポインタです。
コードは、既に解放された何かのデータ構造体を更新し続けています。
[完全な不正 / Full corruption]
不正ブロックが隣接しており、その中のバイト列が全く変わってないなら
(ゼロのような、一般的には正しいか間違ってるかわからないデータが頻繁に存在する数バイトを除いて)
他のどこかからデータ構造を初期化されたか、リセットされたか、またはコピーしてこられたのでしょう。
[ユニットベクトル / Unit Vectors]
ベクトルは、一般的に3つの浮動小数点型を(アレンジして)使用します。
そして、ベクトルの一般的なサブグループはユニットベクトルです。
ユニットベクトルはメモリでかなり認識可能です。
3つの小さい浮動小数点(+1.0~-1.0の範囲)から成り
頻繁に16進表記で『3』か『B』から始まるからです。
ここに、ユニットベクトルに関する例があります。
ユニットベクトルが、周りの文字列と調和せずに浮きだっているのがわかります。
5c6b6369 73636f64 6d61675c 6e697365 ick\docs\gamesin 3e6fdb1a bd0ee1b0 3f7909cd 6f635c6b .Ûo>°á.½ Í.y?k\co 655c6564 706d6178 5c73656c 6d617865 de\examples\exam |
16進のダンプを見て、何か問題があるかどうかをすぐに明確には出来ません。
しかしながらこの例では、テキスト表示コラムからいくつかのゴミバイトがパス名の中央にあるように読み取ることが出来ます。
次にゴミバイトの文字列をもっとよく見てみると
2つの「3から始まっている」浮動小数点と、「Bから始まっている」浮動小数点があります。
・これは、私たちが「少ない数(ことによるとユニットベクトル)」に対処しているといういう、非常に良い指令です。
では、浮動小数点視点に切り替えて見てみましょう。
2.6502369e+017 1.8019267e+031 4.3599426e+027 1.8062378e+028 |
これはエラーの本質を裏付けます。
ここには、範囲が-1.0~1.0までのに3つの浮動小数点があります。
さらに、この3つを加算するとおよそ1.0になるということがすぐに計算できると思います。
したがって、ベクトルの長さは1.0───つまりそれはユニットベクトル───です。
エラーの原因と影響
だいたいのエラーの本質をいったん決定したなら
エラーを引き起こしたコード部分を特定する必要があります。
エラーの場所を直接観測できないならば
あなたは証拠として、疑わしげなコードをいくつか選択して観測しても良いです。
私たちは、考えられるかもしれないコードの片の分野を限定するために、
不正の最も一般的なダイレクト原因を見て、各原因がどう現れるかを調べるべきです。
バッファ・オーバーラン
バッファオーバーランは恐らく最も一般的に発生するバグです。
あなたは、ハッキング世界でよく「バッファを食い物にする方法(“buffer exploits”)」についてを耳にしたコトと思います。
ここでプログラマは、入力データのサイズが目的地のスペースに収まるかどうかチェックするのを忘れました。
するとデータは、バッファをオーバランし───ことによるとコードに使用される何らかのスペースを上書きします。
何らかの適切なコードをデータの終わりに加えることによって
勤勉なハッカーは、彼自身のコードのいくつかをアプリケーションに注いで、それを制御できます。
ゲーム開発では、バッファオーバーランが利用される事例は少ないです。
インターネットを介してデータを受け渡すカタチでない場合は、ですが。
しかしながら、それでもバッファオーバーフローは非常に重要なバグの原因です。
ダメなポインタ
ポインタの値が間違っているなら、それはメモリを崩壊させることができるということです。
(そのポインタを使用する人なら誰でも、悪いデータを提供することができるのですから)
ポインタ値は、多くのケースで「ダメ」になることがあります。
[ぶら下がりポインタ / DanglingPointer]
メモリブロック(の一部が)解放、またはアロケートからハズレタ時
まだ、そのブロックを参照しているいくつかのポインタ(またはその中のオブジェクトに)がある場合
このときの、そのポインタ郡を『ぶら下がりポインタ』と呼びます。
ポインタの値は変化せずとも、そのぶら下がり状態になったポインタは
もう有効なデータを指すことはありません。
[間違ったポインタ演算 / Incorrect Pointer Calculation]
間違った計算方法を使用するか、ほかの間違った値を使うことで
問題のあるポインタを生成することが出来ます。※16
ポインタの値が不当に計算されることになります。
またポインタ演算は、目標バッファの(バッファオーバーフローしてる)範囲から
ポインタを返すかもしれません。
[ぶっ壊れポインタ / Corrupt Pointers]
ポインタを格納したメモリが、何か関係のない原因で崩壊していた場合
原因が連鎖・拡散し、不正が不正を引き起こす場合があります。
ダメなローカルポインタ
ポインタがローカルスコープを持つオブジェクトに作成されると
そのオブジェクトがスコープにある間だけ、そのポインタは有効です。
下記コードを参照してください。
void CheckThing(CThing *p_x) { CThing p_thing; p_thing = *p_x; if (ThingCheck(p_thing)) { AddToList(p_thing)); } } |
ここでの局所変数p_thingは何らかの一時的な目的のために使用されています。
しかしながら関数が実行中の間、変数は何らかのグローバルなリストに追加されたのち、関数は戻ります。
結果として、何らかの現在位置を示すポインタがあって
それは、スタックによって使用されるメモリを示しているということです。
これは、当面の問題にはならないでしょう。
それから関数は戻り、次にスタックポインタは上位のメモリへ引っ込むことでしょう。
安全にp_thingのインスタンスをスタックに残して。
そして、以下の2つのうち片方が起こるかもしれません。
※17
オブジェクトは崩壊します
オブジェクトポインタp_thingが、もう正しいものとは呼べない状態で
でも、バイナリ画像はまだメモリにあって
コードは、いったんメモリにロケートされると
問題なくスタックに持っていくことが出来ます。 ※18
この時、オブジェクトは崩壊するかもしれません。
これがある意味でメモリ破壊バグではなく
(legal=規約に沿って)正しくコーディングされており、正しい場所で使われていた場合でも
それは非常に同様のメモリ破壊バグを反応させます。
スタックは崩壊します
オブジェクトがリストにあります。
それに伴い、おそらくいくつかのリスト操作を行うコードがあるでしょう。
スタックがこの(不正な)ポインタを下った場所を指し、リストでそのオブジェクトをアップデートする場合など。
この場合にオブジェクトをアップデートすると、スタックによってlegalに使用されている何らかのメモリが崩壊するでしょう。
それはリターン先であるかもしれないし、保存しているレジスタ値であるかもしれません。
もしくは、それはなんらかの高位のコードルーチンで使用されている局所変数であるかもしれません。
どれであっても、ファンクションコールスタックがそのポイント(かなり問題の原因から遠いかもしれない)に戻るまで、効果は延期されるでしょう。
スタックオーバーフロー
スタックオーバフローは、いろいろとメモリ破壊を引き起こす可能性があります。
スタックは固定サイズです。
そして、そのサイズをはみ出すと、すぐにそれはメモリを崩壊させ始めます。
何が次に起こるかは、スタック・フレームのサイズ、メモリ上のスタックの位置、そのすぐあとに置かれているデータによるでしょう。
すべてのプラットホームが、どれも同じようにスタックオーバーフローの被害を受け易いというわけではありません。
Win32プラットフォームでは、単にスタックポインタがスタック領域を超えて書きこむだけで、スタックオーバーフロー例外を上げるでしょう。
この挙動を起こすプラットホームでは、デバッグは比較的単純なものです。
呼び出しスタックを見て、その中の一つないし二つくらいの機能を繰り返し読んでいけば、問題の箇所を直接指摘できるでしょう。 ※15
他のプラットホームは、残念ながらそれほど幸いではありません。
PS2には、スタックポインタのための特別な保護はありません。
スタックは頻繁に32MBのメモリの先端に置かれます。
(メモリがスタックのために予約された領域の先に延びているということは、データと場合によってはコードさえ崩壊させることができることを意味しています)
コード不正
既に言及されるように、スタックをメモリ位置と同じ部分に生成すると
現在実行されているコードを上書きするでしょう。クラッシュを引き起こしながら。
デバッガの分解視点で見ると、スタックオーバーフローによるコード不正をしばしば確認することができます。
クラッシュ前のコードは妥当なものでしょうが、クラッシュ後のコードは反復性※18、または違法命令を含んでいます。
これが発生する、最もありそうな原因が、でスタックオーバーフローです。
わずかな不正
スタックオーバーフローが起きる一般的な原因は、再帰処理の暴走です。
それほど一般的でない原因は、非常に大きいスタック・フレームに結合された適度の再帰です。
再帰的なルーチン上に、大きな領域を取る局所変数があるとき、これはプログラマのミスで起こる可能性があります。
例: 例2を参照してください
例2:
class CBuffer { int x[2048]; } int DigTree(CTree *p_tree) |
このDigTreeは、局所変数であるlocal_bufferを持つ再帰的関数です。
インスタンスを新しく作るとき、このlocal_bufferをスタックに作成しなければなりません。
CBufferクラスがメモリを8Kも取るので、スタックオーバーフローが発生するまで、比較的わずかな再帰しか要しません。
スタックサイズができるだけ低く保たれるゲームキューブなどのコンソール上では特に、しばしば64Kまたは以下で落ちてしまいます。
関数が呼ばれたときに、この巨大なCBufferオブジェクトがクリアされていないなら
8K毎にメモリを通して不正な影響を与えることができます。
これは多くの(メモリ破壊の)方法の中でもかなりのやり手であるかもしれません。 ※19
まず第一に、初めてあなたがメモリ破壊の兆候を見たときに、それがただの一例であるように思えるかもしれません。
───あなたに「スタックオーバーフロー」という選択肢を考えさせないような。
第二に、それはあなたがスタックオーバーフローがないかどうかチェックするための、どんなテストも踏み越えることができます。
あなたは、しばしば、いくつかのマジックナンバーをスタックの下部に置いて、
スタックがあふれたかどうか検出し、そのマジックナンバーがまだそこにあるかどうか確認するでしょう。
ゲームが再帰処理中にクラッシュしない場合、スタックポインタは正常なアドレスに戻るでしょう。
そして、メモリ破壊が後に何らかの問題を引き起こすまで、あなたのコードは陽気に走っていられるでしょう。
三番目に、大きいスタック・フレームの場合、それが暴走再帰であれば実行されているコードを踏み越えるかもしれません、
広範囲のコード不正を引き起こして、実際に不正を引き起こしたコードまではまだクラッシュしていない状態で。
このとき、スタックオーバーフローの可能性に目を向けるべきなのですが、それほど原因は明白にならないでしょう。
崩壊したスタック・フレームのスタック分析は、崩壊させられたコードの位置を示すでしょうから。
与えられた情報から、その不正の原因を出来る限り掲出します。 ※20
※1 訳に自信なし。 ただクラッシュはヤバいっていう風に取れる。
※2 訳に自信なし。 unmapped が何を指すか判らん。
※4 全体的な"legal"の訳が法的の意味なのか、安全的なの意味なのか、それともクリティカルセクションみたいなノのことなのかわからん。
※5、※6 訳に自信なし
※7 「Manual method」はなんて訳せばいいかわからんかった。
※8 原文、片方が5バイトになってるけどたぶん誤植の気がする。
※9 自信なし
※10 自信なし
※11 アセット=資産だけど・・・・・・ファイル資産?
※12 よくわからん。 自信なし。
※14~ 自信なし。
#3/3 全訳終了