読者です 読者をやめる 読者になる 読者になる

落書きノート

ふと自分が気になった事を書いてます

複雑な処理を読んでみる i386編

以下のようなCソースファイルがあります。

int call_complex1()
{
  return return_arg1(0xfe) + 1;
}

int call_complex2(int a, int b)
{
  static_value = return_arg1(b);
  return b;
}

これはi386で逆アセンブルするとどうなるのでしょうか…というのが今回の話題です。call_complex1から逆アセンブルしてみます。

00fe150d <call_complex1>:
  fe150d:    68 fe 00 00 00         push   $0xfe
  fe1512:    e8 26 ff ff ff         call   fe143d <return_arg1>
  fe1517:    83 c4 04               add    $0x4,%esp
  fe151a:    40                     inc    %eax
  fe151b:    c3                       ret    

先頭の2行目でpush命令が呼ばれていてスタック操作していますが、これはi386では引数もスタックで渡されるため、スタックに0xfeという引数を積んでいる処理です。つまりスタックへのレジスタ値の退避などではありません。

引数の設定の後はcall命令で関数呼び出しを行っています。4行目のadd命令はスタックに残っている引数の値(0xfe)を廃棄して、スタックポインタの位置をcall_complex1()の呼び出し前に合わせるためのものです。つまりスタックフレームの解放です。

そして5行目でEAXレジスタで渡された戻り値をインクリメントし、6行目のret命令で呼び出し元に戻っています。

こうしてみるとi386も、引数をスタックで渡しているためにスタック操作している以外は、H8(H8についてはまだ書いてませんが…。)と同じようなアセンブラになっているようですね。

call_complex2()のi386向けアセンブラは以下のようになっています。

00fe151c <call_complex2>:
  fe151c:    53                     push   %ebx
  fe151d:    8b 5c 24 0c             mov    0xc(%esp),%ebx
  fe1521:    53                     push   %ebx
  fe1522:    e8 16 ff ff ff         call   fe143d <return_arg1>
  fe1527:    83 c4 04               add    $0x4,%esp
  fe152a:    a3 00 18 fe 00          mov    %eax,0xfe1800
  fe152f:    89 d8                   mov    %ebx,%eax
  fe1531:    5b                      pop    %ebx
  fe1532:    c3                       ret    

どうも「ebx」というレジスタが不揮発性の扱いになっていて、先頭でpush命令によりスタックに退避されているようです。実際にi386は「EBX」というレジスタを持っています。

そして3行目では、「スタックポインタ + 12」の位置から値をロードしてEBXレジスタに格納しています。

関数呼び出し時にはスタックポインタの指す先に戻り先アドレスが、スタックポインタ + 4の位置に第1引数が格納されています。さらに第2引数はスタックポインタ + 8の位置に格納されています。しかし2行目のpush命令でスタックポインタが4バイト減算されているため、3行目の実行時にはスタックポインタ + 12の位置には第2引数があります。

ということは3行目で第2引数の値がEBXレジスタにロードされ、さらにその値が4行目でスタックに格納されます。i386では引数はスタックで渡されますから、4行目ではスタックにEBXレジスタの値を積むことで、return_arg1()の呼び出し時の引数をスタック上に設定しているわけです。

EBXレジスタに保存した値は、8行目でEAXレジスタに設定されます。これによりcall_complex2()の第2引数は戻り値として設定されることになります。EBXレジスタはその後に9行目でスタックから値を復旧し、呼び出し前の状態に戻ります。

そう考えると、第2引数について行っている処理の内容はMIPSPowerPC(まだ書いてませんが…。)と大差はなく、単に値の取得先がレジスタでなくスタックになっているだけです。まず不揮発性のレジスタを利用するのでそれをスタックに退避しておき、さらにそのレジスタに第2引数の値を保存しています。return_arg1()の呼出し後は不揮発性レジスタに保存していた第2引数を戻り値として設定し、最後に呼び出し前の値をスタックから復旧することで、元に戻しているわけです。

こうしてみると、H8やi386では戻り先アドレスを保存する必要がない分だけ処理が少なくなっていますが、不揮発性のレジスタに関する処理はMIPSPowerPCと同じ(ARMやSHでも同じ)ということのようです。