アセンブリ言語の主要な命令とスタックフレーム

参考書籍 : デバッガによるx86プログラム解析入門【x64対応版】


・主要な命令
1, MOV命令 : メモリやレジスタ間でのデータ転送
MOV ソースオペランド, ディスティネーションオペランド と記述する。

MOV SWORD PTR [4020000], EAX
MOV EAX, 5

CXレジスタからEAXレジスタへのデータ転送のように、転送先と元でサイズが異る場合は、
MOVSX(算術転送)、MOVZX(ゼロ拡張して転送)命令が使用される

2, CMP命令 : 比較命令
ディスティネーションオペランド - ソースオペランド の除算により比較する。
比較結果はステータスフラグとしてEFLAGSレジスタに格納される。

CMP SWORD PTR [4020000], EAX
CMP EAX, 5

オペランドにはメモリ上の格納値やレジスタ、即値が使えるが、メモリ上の格納値同士の比較はできない。

3, TEST命令
ディスティネーションオペランドとソースオペランドをAND演算で比較する。
比較結果はCMP命令と同じく、ステータスフラグとしてEFLAGSレジスタに格納される。

TEST EAX, EAX

この例では、EAXレジスタの格納値が0の場合のみ演算結果が0になる。

4, JMP命令 : 無条件ジャンプ
オペランドで指定したアドレスに無条件でジャンプする。
ジャンプ先アドレスは、ジャンプ命令の次の命令からの相対アドレスで指定する。

JMP 401500

相対アドレスは符号付きの値を用いるため、負の数を指定して前方向にジャンプもできる。

5, J**命令 : 条件付きジャンプ
JNZ(ゼロでない)、JG(より大きい)などの命令がある。
EFLAGSレジスタのフラグの情報を条件としてジャンプする。
Jのあとに指定できるのは、N(Not)、E(Equal)、Z(Zero)、G(Greater)、L(Less)、などがある。

6, CALL命令 : 関数の呼び出し
関数呼び出してCPUに実行させる。関数内でRET命令が呼び出されると呼び出し元に処理が戻る。
CALL命令実行時に、EIPレジスタに格納されたCALL命令の次の命令のアドレスが、自動的にスタックにPUSHする。
RET命令実行時にはスタックに格納されていたアドレスがPOPされ、EIPレジスタの値が復元される。

7, 算術演算命令
算術演算命令には、ADD, SUB, MUL, DIVなどがある。

ADD EAX, 5
SUB DWORD PTR [402148], 1
MUL EBX
DIV EBX

ADD命令とSUB命令はディスティネーションオペランドとソースオペランドを演算し、
結果をディスティネーションオペランドに格納する。
MUL命令とDIV命令は、暗黙的にEAXレジスタを使い、演算の補助にEDXレジスタを使う。
MUL EBXならば、EAXレジスタの値とEBXレジスタの値を乗算し、結果の上位32bitをEDXに、下位32bitをEAXに格納する。

8, 論理演算命令
AND, OR, XORなどがある。

AND DWORD PTR [402148], 0FFFF
OR EAX, 8
XOR EAX, EAX

演算結果はディスティネーションオペランドに格納される。
XOR EAX, EAXのように、同じレジスタにXOR命令を実行すると、その値が0になる。

9, NOP命令
No OPeration命令の略
何もしないわけではなく、EAXレジスタとEAXレジスタの内容を入れ替えるという意味のない処理を行う。
NOP命令は機械語で0x90であり、既存のコードを0x90で置き換えることにより、簡単に無効化できる。

10, LEA命令
Load Effective Address命令の略
ソースオペランドの内容を演算した後、結果をディスティネーションオペランドに格納する。

LEA EAX, [EBX+ECX*4]

算術命令とは異なり、ステータスフラグは更新されない。

11, ストリング操作命令とリピートプリフィックス
ストリング操作命令は、データブロックを扱うための命令。
MOVSB/MOVSW/MOVSDやCMPSB/CMPSW/CMPSD命令がある。
リピートプリフィックスは、繰り返し処理を行うための命令。
REP/REPE/REPNE命令があり、ストリング操作命令とリピートプリフィックスは組み合わせて使われる。

MOV ESI, 402150
MOV EDI, 405000
MOV ECX, 20
CLD
REP MOVSD

CLDにより、繰り返し処理によりESIレジスタとEDIレジスタがインクリメントされるようになる。
REP命令により、ECXが0でなければMOVSDを実行し、MOVSD実行後にECXをデクリメントする。
つまり、
アドレス 402150からアドレス 405000へ4バイト転送、ECXを 0x1Fに
アドレス 402154からアドレス 405004へ4バイト転送、ECXを 0x1Eに
アドレス 402158からアドレス 405008へ4バイト転送、ECXを 0x1Dに
アドレス 40215Cからアドレス 40500Cへ4バイト転送、ECXを 0x1Cに
...
という処理になる。



・スタックの仕組みとスタックフレーム
ESPレジスタを利用して、メモリ領域をスタックとして使うことができる。
スタックにはデータが下位アドレスから上位アドレスに積み上がる。
スタックにデータを積むにはPUSHを使い、POPで取り出す。
ESPレジスタは常にスタックの最上位のアドレスを指し、EBPがスタックの最下位のアドレスを指す。

呼び出し規約cdeclでは、CALL命令で関数を呼び出す前に、引数をスタックにPUSHする。
関数の戻り地はEAXに格納される。
関数内でローカル変数を使うときは、ESPをローカル変数のサイズだけ減らすことで、領域を確保する。
関数は最初にEBPをPUSHする。EBPを基準に相対アドレスで変数にアクセスする仕組みを、スタックフレームと呼ぶ。
関数の呼び出しをアセンブラで表現すると、以下のようになる。

; 関数呼び出し
PUSH 2 ; 第2引数
PUSH 1 ; 第1引数
CALL XXXXXXXX
ADD ESP, 8 ; 引数で使ったスタックのサイズ分を修正
; 関数本体
PUSH EBP ; EBPレジスタの値(アドレス)をフレームポインタとする。
MOV EBP, ESP ; ESPをEBPに退避、以後ローカル変数にはESPから相対でアクセスする。
SUB ESP, 8 ; ローカル変数(ここではDWORD2つ分)の設定
PUSH EDI ; 関数の処理でEDIを書き換えないように退避
PUSH ESI ; 同じくESIを退避

この時点でスタックフレームは以下のようになっている。

DWORD PTR [EBP-10] = ESIの元の値
DWORD PTR [EBP-0C] = EDIの元の値
DWORD PTR [EBP-8] = ローカル変数2
DWORD PTR [EBP-4] = ローカル変数1
DWORD PTR [EBP] = EBPレジスタの値(フレームポインタ)
DWORD PTR [EBP + 4] = 関数のリターンアドレス
DWORD PTR [EBP + 8] = 第1引数
DWORD PTR [EBP + 0C] = 第2引数

呼び出された関数は、最後に以下の処理を行う

POP ESI
POP EDI
MOV ESP, EBP ; スタックポインタを復帰、ESPはローカル変数の下位になる
POP EBP ; EBPを復帰
RET

なお、RET 10のようにして、RET命令で同時にスタックの破棄をする場合もある。