ぱたへね

はてなダイアリーはrustの色分けができないのでこっちに来た

自己書き換えコード

せっかく問題のNoteに、自己書き換えコードについて書いてあるのでもう少し追ってみましょう。shinhさんのところで紹介されているmain=195;が、自己書き換えコードの一例です。

MIPSでの自己書き換えコード

Exercise 2.5で使われているプログラムは、最後のsllを実行した後、次の命令を実行しようとして暴走してしまいます。エミュレータ上では、Attempt to execute non-instruction at 0x00400050 と表示されますが、実機ではおそらく未定義命令の例外が発生しまいます。例えばここをmainからのリターンコードに書き換えることで、プログラム自体を正常終了させることができます。main = 195のように、shifterに書かれているコード(sll)をリターンに書き換えてみましょう。


例えばこんな感じで。

.data
ret:	.word 0x03e00008
.text
main:
	lw $s0, ret # レジスタs0に0x03e00008をロード
	sw $s0, shifter # shifterのアドレスに、レジスタs0の値(0x03e00008)をストア
shifter: 
	sll $s0,$s1,0 # 実行されるときはシフト命令ではなく、ja $raに書き換わっている。これで、mainから抜け出す。

MIPSの場合、関数から戻る命令はja $raなので、そのビットパターン 0x03e0008を用意します。shinhさんの所にも書いてありますが、データシートを探るのではなく適当に関数呼び出しを書いてリターンっぽい所のダンプを見て値を調べています。これをエミュレータ上でSTEP実行すると、sw $s0, shifterの命令を実行した後に、sll $s0,$s1,0が、jr $31に変わるのが分かります。命令が変わった後に、jr $31が実行されmainからリターンして、exitのsyscallが呼ばれるはずです。


sw $s0, shifter 実行前


sw $s0, shifter 実行後

main=195の解説

shinhさんの所から。

main;
__attribute__((constructor, destructor))
static x() {
    if (main) puts("world!");
    else puts("hello", main = 195);
}

main=195の技は、gccの拡張を利用してmainが呼ばれる前にmainの先頭へx86のret命令を上書きしています。
順を追って書くと、mainはグローバル変数として定義され、まずはconstructor属性が付いた関数xが呼ばれます。グローバル変数は0で初期化されているので、ifは偽になり変数mainに195が代入され、helloが表示されます。constructorから抜けたので、mainがcallされますがそこは既にret命令(195)が代入されているので、命令が実行されmainを抜けます。その後、destructor属性のついた関数x()が再び呼ばれ、今度は変数mainは0でない(195が代入済み)ので、if文が成立し、world!が表示されます。gcc拡張を使っているとはいえ、C言語の枠組みでここまでやってしまうのは素晴らしいです。


俺様デバッガを作りたい人は、ブレークする命令を内部割り込みに置き換えるところから始まるので、最初にマスターする技になります。最近はCPUにブレーク専用の機能がついているので自力で実装することは無いでしょうが、動きを知っておいて損はありません。私も組み込み新人の頃は、「せんせー!ROM化したらブレークかかりません」とか言っていました。誰もが通る道だと信じたいです。