前置きが長くなりましたが,x86(IA-32)について説明を始めます.他のCPUでも概ね似たようなものですが,アセンブリ言語(asm)を記述するときに最低限必要な知識は概ね次の四つです.
- アドレス
- レジスタ
- インストラクションポインタ
- スタック
順次説明します.
アドレス
メモリを読み書きするには場所を指定する必要があります.その場所のことをアドレスといいます.32bitOSでは通常32bitの絶対値(0~4294967295)で指定します.主にC/C++におけるstaticな変数やglobalな変数を扱うときに利用します.仮想メモリや物理メモリなどの話はとりあえず無視してかまいません.必要だと思われたときに勉強してください.レジスタ
CPU内部で利用できる変数のことです.32bit整数を格納する汎用レジスタ,浮動小数を格納するFPUレジスタ,SIMD命令で扱われるMMX/SSEレジスタなどがあります.このうち汎用レジスタが最低限必要なものです.汎用レジスタは8個あり,eax, ebx, ecx, edx, esi, edi, ebp, espと名前も役割も決まっています.当面はこの8個のレジスタのみを意識するだけで十分です.というか8個しかありません.通常の言語では変数名は自由につけられ,しかも好きなだけ使えるのとは大違いです.インストラクションポインタ
CPUがまさに今実行しているアドレスです.命令を実行するごとに自動的に次の命令が格納されているアドレスを指すようになります.条件分岐や関数呼び出しなどはインストラクションポインタを変更することで実現されます.スタック
メモリの一部は,スタック形式(FILO:最初に格納したものは最後に取り出せるデータ形)で扱われる領域として定義されおり,その部分を指します.主にC/C++における局所変数を扱うときに利用されます.スタック領域はある特定値(環境によって異なります)から0に向かう方向に伸びます.0になってしまうとスタックを使い切りスタックオーバーフローとなります.そのスタック領域の先頭アドレスは汎用レジスタespに格納されています(多分Extended Stack Pointerの略).そのためスタックを操作するにはespを扱うことになります.
要はCPUがやっているのは,メモリをレジスタに読みこんで演算し,結果をメモリ/レジスタに格納する,結果によってインストラクションポインタを変更する.これだけです.アセンブリ言語で開発するということはこれらの手続きを一つ一つ丁寧に記述するということです.難しいのではなく,面倒なのですね.
さて,"Hello Xbyak!"を解説します.
push((int)"Hello Xbyak!"); // (C1) call((int)puts); // (C2) add(esp, 4); // (C3) ret(); // (C4)
このプログラムはCでいうところの
void hello() { puts("Hello Xbyak!"); }
と同じです(関数名は関係ないですがとりあえずhelloとします).
- C1.
- C2.
- C3.
- C4.
文字列"Hello Xbyak!"はメモリ上のstaticな領域に格納されています.これをputsの引数として与えるには,そのアドレスをスタック領域に保存します.なぜスタック領域に保存するのかについては今後説明します.とりあえずここでは関数の引数はスタックに格納する必要があることを覚えてください.
そして,データをスタック領域に保存するにはpush命令を使います.intにキャストしているのはXbyakの文法のせいです.つまりこの行は"Hello Xbyak!"が格納されたアドレスをスタック領域に確保することを意味します.スタックで述べたようにスタック領域は0に向かいますので,pushするとespの値が4(byte)減ります.デバッガでpushの前後でespの値が4減っていることを確認してください.
引数がスタック領域に確保にされたのでCのputs関数を呼び出します.Xbyakの文法上,intにキャストしています.この行が実行されるとコンソールに文字列が表示されていることをデバッガで確認してください.
C1でスタック領域に引数を渡したので,このままではスタック領域は4byte減ったままになります.スタックは貴重です.この関数ではもうこの領域は使いませんから開放する必要があります.espに4を足してスタック領域を基に戻しましょう.add esp, 4はC表記のesp += 4;と同じです.
hello()関数を抜けて元のmain()関数に戻ります.そのためにはret命令を使います.デバッガでこの命令を実行するとmainに戻ることを確認してください.
以下にgdbで追いかけたときのログを表示します.実際に自分でも実行してみると理解しやすいでしょう.
(gdb) al 0x804b190: push 0x804a756 0x804b195: call 0x8048710 <puts@plt> 0x804b19a: add esp,0x4 0x804b19d: ret (gdb) p $esp $1 = (void *) 0xbffff2ac // (*) (gdb) si 0x0804b195 in ?? () // (C1)を実行した (gdb) p $esp $2 = (void *) 0xbffff2a8 // (*)に比べてespの値が4減っている (gdb) ni // (C2)callを実行する Hello Xbyak! // 文字列が表示された 0x0804b19a in ?? () (gdb) si 0x0804b19d in ?? () // (C3)を実行 (gdb) p $esp $3 = (void *) 0xbffff2ac // espの値が(*)に戻った (gdb) ni main () at t.cpp:19 19 return 0;
今回はアドレス,レジスタとスタック,puts("Hello Xbyak!")の中身を説明しました.次回はサンプルプログラムの説明を続けます.
コメント (1)
(C2)をくりかえせばメッセージが複数回表示されますね。このあたりはアセンブラならではの技(という程ではないかもしれないけど)ですね(CやC++でも最適化で同様のコードになることもあるとは思いますが)。
投稿者: dsk | 2007年10月01日 21:13
日時: 2007年10月01日 21:13