2164 文字
11 分
アセンブリ言語を勉強する
2024-11-16

データ型#

データ型ビット数(バイト数)接頭辞
Byte8bit (1bytes)b
Word16bit (2bytes)w
Dword32bit (8bytes)l
Qword64bit (16bytes)q

1Byteの半分#

ニブル(nybble)は4Bitを表している。つまり16進数で5Dであれば2ニブルと呼ぶ

汎用レジスタ#

x64にレジスタは16個ある。一つのレジスタのビット数は64Bitである。

{rax, rbx, rcx, rdx, rbp, rsi, rdi, rsp, r8, r9, r10, r11, r12, r13, r14, r15}

レジスタにアクセス#

例えばraxレジスタの下位32Bitを参照するにはeaxレジスタにアクセスする。 また下位16Bitを参照する場合はaxレジスタを参照する。 axレジスタの上位8bitにアクセスする場合はahレジスタを参照し、下位8bitにアクセスする場合はalレジスタを参照する。
raxレジスタは全体でこのような構造をしている。

重要

eax,ax,ah,alはそれぞれ独立したレジスタではなく、raxレジスタの一部にアクセスするためのAliasである

全てのレジスタのAliasはこのようになる。1#

64ビットレジスタ下位32ビット下位16ビット下位8ビット
raxeaxaxal
rbxebxbxbl
rcxecxcxcl
rdxedxdxdl
rsiesisisil
rdiedididil
rbpebpbpbpl
rspespspspl
r8r8dr8wr8b
r9r9dr9wr9b
r10r10dr10wr10b
r11r11dr11wr11b
r12r12dr12wr12b
r13r13dr13wr13b
r14r14dr14wr14b
r15r15dr15wr15b

レジスタの役割#

rspレジスタはスタックポインタレジスタとして使用される。
rbpレジスタはベースポインタレジスタとして利用される。
それ以外のレジスタの用途は汎用として利用され、演算の際に一時的な格納場所として使用されたりする。

呼び出し規約#

Windowsでの呼び出し規約2#

関数から呼び出された引数はrcx,rdx,r8,r9の順に格納される。第四引数以降はスタックに格納される。

引数整数型・ポインター浮動小数点
第一引数rcxxmm0
第二引数rdxxmm1
第三引数r8xmm2
第四引数r9xmm3

Linuxでの呼び出し規約3#

関数から呼び出された引数はrdi, rsi, rdx, rcx, r8, r9の順に格納される。第七引数以降はスタックに格納される。

引数整数型・ポインター浮動小数点
第一引数rdixmm0
第二引数rsixmm1
第三引数rdxxmm2
第四引数rcxxmm3
第五引数r8xmm3
第六引数r9xmm3

ptr演算子#

アセンブリではメモリサイズを自分で明記しなくてはい。

mov [rcx], 5dh ;5dは10進数で93

そのため、このような記述ではrcxレジスタのどのデータサイズで5を格納するのかが分からないためエラーとなる。

mov eax, 5dh
mov [rcx], eax 

このような記述をする必要がある。

mov dword ptr[rcx], 5dh

ptr演算子を使うことでメモリサイズを指定することが出来る。

ptr演算子サイズ(bit)
byte ptr1
word ptr2
dword ptr4
qword ptr8
xmmword ptr16
ymmword ptr32
zmmword ptr64

データ移動命令#

データがある場所から別の場所へ移動させる。
高級言語でいう代入に当てはまる。

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()
{
    test(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関数の中身です。

push ebp
mov ebp, esp
sub esp, 8
mov eax , [ebp+8]
mov eax, [ebp+8]
mov [ebp-4], eax
mov ecx, [ebp+0Ch]
mov [ebp-8], ecx
xor eax, eax
mov esp, ebp
pop ebp
ret

1行目はフレームポインターと呼ばれる関数の実行に関連する情報を管理するポインターです。 これは関数が終了したときに復元を行うための操作です。ebpに戻り先(呼出し元)のアドレスをebpに格納します。 PUSHしているので、EBPレジスタが4減少します。
͏
2行目ではespの値をebpにコピーしています。これは呼び出し元のアドレスをespに格納しています。 これで関数内の宣言なのをebp内で自由に行えます。
͏
3行目でローカル変数を入れるための空間を用意します。
͏
4~9行目で関数内での処理を行っています。
͏
10行目は2行目と逆の操作をしています。これで、呼出し元の関数に戻ります。
11行目は1行目と逆の操作をしています。
͏
[x86アセンブリ言語での関数コール](https://web.archive.org/web/20220813235400/https://vanya.jp.net/os/x86calemistry /)

͏
main関数に話を戻します。test関数が終了した後、main関数ではadd esp, 8から始まります。
これは、espレジスタのスタックを綺麗にするために行われます。 この綺麗にする操作を呼出し先が行うのか呼出し元が行うのかは、コンパイラによって違います。 がしかし、C言語のプログラムのほとんどは呼び出し元が行うことになっています。

配列と文字列の逆アセンブル#

int nums[3] = {1,2,3}

配列はアドレスに配列の先頭アドレス + 変数のサイズ × 要素番号で決まります。

Footnotes#

  1. x64 アーキテクチャ #レジスタ

  2. x64 アーキテクチャ #呼び出し規則

  3. Linux x64 Calling Convention: Stack Frame