最も簡単な逆アセンブル(再帰)

最も簡単な逆アセンブル(ヒープ) - あしのあしあと」の続き。今回は、再帰の場合を実験してみる。
題材は、階乗を求める関数 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) のかけ算、のように実行される。


久しぶりだったので、時間がかかってしまった。とは言いつつも、少しずつ難易度の高いソースコードを逆アセンブルしていかないとだ。