コンピュータの基礎
コンピュータの最小単位はBit
8ビットのグループは1Byteと呼ぶ。1Byteは2つの16進数で表現される。
例えばb01011101は16進数では5Dと表現される。そして、5(0101)とD(1101)はそれぞれニブルと呼ばれる。
Byte以外に
2Byteをword
4ByteをDouble word
8Byteをquadword
と呼ぶ
データの解釈
b01011101
は
16進数で5D
10進数で93
ASCII Codeでは]
を表し
マシンコードでは(mov dbp, esp)
を表す。
汎用レジスタ
x86 CPUには8つの汎用レジスタがある。
{EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI}
これらは、それぞれ4バイトです。
各レジスタの下位2バイトには
{AX, BX, CX, DX, SP, BP, SI, DI}
としてアクセスできます。
また、8つの汎用レジスタの下位2バイトの下位1バイトは
{AL, BL, CL, DL}
8つの汎用レジスタの下位2バイトの上位1バイトは
{AH, BH, CH, DH}
としてアクセスすることができる。
命令ポインタ
CPUにはEIPという特別なレジストリがある。これは次の命令のアドレス場所を指し示す。
データ移動命令
データがある場所から別の場所へ移動させる。
高級言語でいう代入に当てはまる。
mov dst, src
レジスタへ定数を格納する
mov eax , 10
mov eax , 64h ;16真数の値0x64をeaxレジストリに格納している。
レジストリからレジストリへの値移動
mov ebx, 10
mov eax, ebx
これは、ebxに定数10を格納した後に、ebxの中身をexaへ格納している。
つまり、eax = ebx , 10 = eaxとなる。
メモリからレジストリへの値移動
例えば、C言語でint a = 64;を行ったとする。
そして、変数aはメモリ0x403000に格納されたと仮定する。
この場合、メモリからレジストリへの値移動をさせると
mov eax, [0x403000]
となる。[0x403000]はint(4バイト)でメモリに格納されているので、movを使った時もexaには4バイト移動する。
アセンブリコードで4バイトを指定することはない。
重要なのは括弧で囲まれた全てがアドレスになること。
mov eax, [0x403000]
mov ebx, [eax]
これは、eaxに入れた0x403000のアドレスが、後のmov ebx, [eax]で、ebxにも同じアドレスが格納されている。
mov ebx,[0x403000]
mov ecx,[0x705000]
mov eax,[ebx+ecx]
これは3行目で二つのレジスタを加算しているので、eaxのアドレス値は0xB08000となる。
mov ebx,[ebp-4]
例えば、ebp値に0x403000があるとする。するとebp-4は元のebpのアドレス値から0x4加減したアドレスを指す。
また、ebpはべースポインタレジスタなので、[ebp-4]のアドレスにあるデータをebxに格納する。
分岐と条件
無条件ジャンプ
常にジャンプをする。JMP命令はC言語のGOTO文に似ている
JMP <jump address>
条件ジャンプ
x86では第一引数から、第二引数を引き算し、結果をフラグを設定する。
mov eax, 5
cmp eax, 5
eaxから5を引き算し、結果は0になるため、ゼロフラグを設定するが、結果は格納されない。
関数の呼出し
call <some_funciton>
<some_funciton>にはアドレスが含まれる。
関数の終了
関数から呼出し元に戻るときはRET命令
を使います。
スタック
スタックは「後入れ後出し」構造を採用している。つまり、スタックに入れた最新のデータが、スタックから取り出される最初のデータになる。
PUSH命令でデータをスタックに格納。32Bitの場合、4バイトずつデータを格納する。
POP命令でデータをスタックから取り出す。32Bitの場合、4バイトずつデータを取り出す。
スタックは、上位アドレスから下位アドレス方向へ積み上げていく。
スタックが作られると、一番先頭の上位アドレスを指す。
ESPレジスタ
ESPレジスタは関数の呼出しと戻りを管理する役割を果たす。
push 3
push 4
pop ebx
pop edx
例えば、ESPレジスタがスタックの最上部(0xff8c)を指してるとします。
push 3
を実行すると、ESPは4減少して、3という値がスタックに格納されます。そしてESPは0xff88を指します。
次にpush 4
を実行すると、ESPは4減少して、4という値がスタックに格納されます。そしてESPは0xff84を指します。
そして、pop EBX
を実行すると、現在スタックの先頭(0xff84)に格納されているデータをEBXレジスタに格納されて、ESPは4増加します(現在のESPは0xff88)。
同様に、pop EDX
を実行すると、ESPは4増加して、格納されていたデータはEDXに格納されます。そしてスタックの先頭は0xff8cを指します。
関数パラメータと戻り値
main関数のアセンブリ言語を見てみます。
int main()
{
tesdt(2,3)
return 0;
}
push 3
push 2
call test
add esp, 8
xor eax, eax
1~3行目は関数呼出しを意味しています。引数は右から左にスタックへPUSHします。
4行目はtest関数が終了した後の処理になります。関数が実行された後に元のアドレスに戻るために、
espにpushした数4Byte分だけ加算します。(今回の場合、test関数は引数が2つ必要なので24Byte=8となります。)
このような処理をリターンアドレスと言います。
int test(int a, int b)
{
int x, y;
x = a;
y = b;
return 0;
}
次にtest関数の中身です。
1 push ebp
2 mov ebp, esp
3 sub esp, 8
4 mov eax , [ebp+8]
5 mov eax, [ebp+8]
6 mov [ebp-4], eax
7 mov ecx, [ebp+0Ch]
8 mov [ebp-8], ecx
9 xor eax, eax
10 mov esp, ebp
11 pop ebp
12 ret
1行目はフレームポインターと呼ばれる関数の実行に関連する情報を管理するポインターです。
これは関数が終了したときに復元を行うための操作です。ebpに戻り先(呼出し元)のアドレスをebpに格納します。
PUSHしているので、EBPレジスタが4減少します。
͏
2行目ではespの値をebpにコピーしています。これは呼び出し元のアドレスをespに格納しています。
これで関数内の宣言なのをebp内で自由に行えます。
͏
3行目でローカル変数を入れるための空間を用意します。
͏
4~9行目で関数内での処理を行っています。
͏
10行目は2行目と逆の操作をしています。これで、呼出し元の関数に戻ります。
11行目は1行目と逆の操作をしています。
͏
x86アセンブリ言語での関数コール
͏
main関数に話を戻します。test関数が終了した後、main関数ではadd esp, 8
から始まります。
これは、espレジスタのスタックを綺麗にするために行われます。
この綺麗にする操作を呼出し先が行うのか呼出し元が行うのかは、コンパイラによって違います。
がしかし、C言語のプログラムのほとんどは呼び出し元が行うことになっています。
配列と文字列の逆アセンブル
int nums[3] = {1,2,3}
配列はアドレスに配列の先頭アドレス + 変数のサイズ × 要素番号
で決まります。
さらなる学習のため
Learn C
C Programming Abslute Beginner’s
Assembly Language
x86 Disassembly