Binary2.0勉強会 5

間があいた。
この辺から大分内容の信憑性が落ちるうえに説明がさらにいい加減になってくると思いますが、ちゃきちゃき参りましょう。


今回は、前回までみていたコードは一旦忘れて、アセンブラでの関数の扱いに関して簡単に触れます。
関数がどのように実装されるのかをまず確認しましょう。
各関数内の処理は、機械語の命令列としてメモリ上に適当な感じで配置されてます。関数を呼ぶには、プログラムカウンタ(実行される命令のあるアドレスです)を適当な感じで書き換えます。多分。ちなみにプログラムカウンタはレジスタの eip が保持していますが、まあ意識することはないと思います(これ lea とかで書き換えたりしたらジャンプするのかな?)。
書き換えられた先のアドレスに配置された命令を実行していって、関数の処理が終了したとしましょう。処理を終えれば、呼び出し元に帰らないといけません。ですが、帰るべきアドレスを知っていないとプログラムカウンタを書き換えられないので迷子になってしまいますね。何らかの形で呼び出し元から帰ってくるべきアドレスをもらわないといけません。他にも、引数や返り値、受け渡ししないといけない値は複数あります。しかしレジスタにそんな余裕はないし…どうなっているのか。
実は、関数を呼んだり、関数の呼び出し元に戻ったりする際に、レジスタやスタックをどういった状態にしておかないといけないというような決まりがあって、これを関数の呼び出し規約とかいいます。この約束を皆守っているので、迷子になったり値の受け渡しで困ったりしないですむわけですね。
呼び出し規約にも種類があるのですが、C と x86 の組み合わせの場合、基本的には cdecl というものが使われていると思いますメイビー。よく知りません。
適当に規約の内容をまとめると、

  • 引数はスタックにぽんぽんと右から積む
  • 返り値はEAXレジスタに積む
  • EBX, EBP, ESP, EDI, ESI の 6 つのレジスタは値を変えてはいけない。

というような感じです。
さてこれで、値の受け渡しに関しては解決しました。後は適当にスタックに引数つんでジャンプして、帰る時も適当にジャンプするわけですが、帰るアドレスをどこに取っておくかという点がまだ残っています。
この辺はそもそも専用の命令が用意されていて、それが前回にもでてきた ret と call です。
call は、戻るべきアドレスをスタックに積み、プログラムカウンタをオペランドで示されたアドレスに書き換えます。これでぴょーんと処理が飛びます。
ret は、プログラムカウンタをスタックトップに積まれているアドレスに書き換えます。ぴょーんと帰ってきます。
例えば下のような関数があったとすると、

int hoge(int i, int j);

関数を呼ぶときはスタックをこんな風にしてやるわけです。

----------------
|  j |  i |addr|
----------------

帰るときは EAX レジスタに返り値をつんでおいて ret すると、スタックトップにあるアドレスに戻る、と。
さてこれで困るのは、関数内でスタックを使いたい時です。関数内で pop したりしてスタックポインタが変わってしまうと ret で帰れなくなってしまいます。
そういった問題に煩わされないように、各関数ではまず EBP と ESP を書き換えて、他の関数とはスタックを分けて使うことになっています。これがまあ俗にいう「スタックフレーム」ですね。勿論関数から帰る時にはそれらを復元してやります。
この辺も enter と leave といういかにもな命令が用意されています。
enter は、 push EBP; EBP = ESP; ESP -= size します。
leave は ESP = EBP; pop EBP します。
…なのですが、 gcc の吐くアセンブラは何か基本 enter は使いません。 enter は第二オペランドでネストレベルの指定ができて、まあ何か色々やってくれるんですが、そんなことはやらんでいいということでしょう。leave もローカル変数ないときは使われません。ESP 変わらないないからですね。
この辺まだまだ書くことはあるんですが、ひとまず最低限は書いた気がするのでここでおしまい。そもそもスタック絡むと文章だけで説明するのが異常にだるいのでもう適当でいい。暑いし。