アセンブリ言語の主要な命令とスタックフレーム
参考書籍 : デバッガによる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命令で同時にスタックの破棄をする場合もある。