「最も簡単な逆アセンブル(ヒープ) - あしのあしあと」の続き。今回は、再帰の場合を実験してみる。
題材は、階乗を求める関数 fact。これは、再帰的に、次のように書ける。シンプル。引数のチェックとか、とりあえずなしで。
int fact(int n) { if (n == 1) return 1; return n * fact(n - 1); } int main(void) { return fact(7); }
早速、逆アセンブル。今回も、セキュリティチェックを無効(/GS-)にしている。
まずは main 関数。セキュリティチェックがないと、本当にすっきりするなぁ。
_main: (01) mov ecx,6 (02) call 01151000 (03) lea ecx,[eax*8+00000000h] (04) sub ecx,eax (05) mov eax,ecx (06) ret
ECX に 6 を格納して、即 fact 関数をコール。しかしなぜ、7 ではなくて、6 なんだ?
(2) 行目で、fact 関数を呼び出して、6 の階乗を求める。(3) 行目で EAX の中身(6 の階乗である 0x02D0)を 8 倍して、ECX に格納(0x1680)する。そして最後に ECX から EAX を引く。確かに、つじつまは合うのだが、なぜこんなことを?
次に、fact 関数。たった 12 行。細かくみていく。
まず、ESI に 6 を入れて、(1)(2)(3)(4)(8)(9) を処理する。次に、ESI の値をデクリメントして、(1)(2)(3)(4)(8)(9) を処理する。これを繰り返す。このとき、fact(01151000h) をコールするたびに、「コール命令の次」のアドレスをスタックに積む。さらに fact の先頭で、ESI に格納されている値をスタックに積む。なので、スタックの状況は、こんな感じになる。
なんか、絵が微妙だなぁ。「fact(2) で使用」ってあるけど、「C のプログラムにおける fact(2) のかけ算の部分で使用」って意味。
ここまでが、階乗のかけ算を実行する準備をしているイメージ。計算の準備も最終段階、ESI に 2 を入れて (1) 行目から処理を実行すると、ついに、(5)(6)(7) 行目の処理に入る。これで、準備が終わった。あとは、かけ算の実行を繰り返すだけ。POP 命令で ESI の値を取ってきて、RET 命令でスタックにある、かけ算を実行するアドレスにジャンプする。これを繰り返すだけ。うまくできている。
コードの実行のイメージは、こんな感じ。
fact(6) の準備処理、fact(5) の準備処理、… fact(2) の準備処理と繰り返し、fact(2) のかけ算、fact(3) のかけ算、… fact(6) のかけ算、のように実行される。
久しぶりだったので、時間がかかってしまった。とは言いつつも、少しずつ難易度の高いソースコードを逆アセンブルしていかないとだ。