アセンブリは命令セットの助記符であり、コンパイラ + スタイル + 命令セットアーキテクチャ(ISA)の組み合わせによって言語の特性が決まりますが、統一された標準はありません:
コンパイラ:
1)MASM:Windows プラットフォームのみをサポートし、オンデマンドコンパイルを完全にサポートする唯一のコンパイラで、bin 形式の出力はサポートしていません。
2)NASM:クロスプラットフォームで、さまざまな出力形式(bin/coff/omf/elf/…)をサポートしています。
スタイル:
1)Intel スタイル
2)AT&T スタイル
主流の命令セットアーキテクチャ:
1)x86-64/x64/amd64/Intel64
2)ARM64/AArch64
3)RISC-V
4)MIPS
x86 アセンブリ#
Intel スタイルのアセンブラを例にとります。
メモリとアドレッシングモード#
.DATA
静的データ領域の宣言#
データ型修飾原語:
)DB
: バイト,1 バイト
)DW
: ワード,2 バイト
)DD
: ダブルワード,4 バイト
)アセンブリでは一次元配列のみ、二次元および多次元配列はありません。一次元配列は実際にはメモリ内の連続した領域です。さらに、DUP
と文字列定数も配列を宣言する 2 つの方法です。
.DATA
var DB 64 ; 1バイトサイズの変数 var を宣言し、64で初期化
var2 DB ? ; 初期化されていない1バイトサイズの変数 var2 を宣言し、その初期値は未定義で、プログラム内で明示的に値を設定するまで未定義
DB 10 ; 値が10の1バイトサイズの定数を宣言。バイトの位置は var2 + 1。ラベルがないため、この値はラベルを通じてアクセスできません。
X DW ? ; 初期化されていない1バイトサイズ(16ビット)の変数 X を宣言し、初期値は未定義で、プログラム内で明示的に値を設定するまで未定義。
Y DD 30000 ; 4バイトの値を宣言し、Yという位置を指し、30000で初期化。
Z DD 1, 2, 3 ; 配列Zを宣言し、各要素は4バイトサイズで、1, 2, 3で初期化。Zラベルはデータストレージの開始位置を示し、Z+インデックス*4が要素のアドレスです。
bytes DB 10 DUP(?) ; 配列bytesを宣言し、各要素は1バイトサイズ;10 DUP(?)は10個の初期化されていないバイトを宣言します。
arr DD 100 DUP(0) ; 配列arrを宣言し、各要素は4バイトサイズ;100 DUP(0)は100個の初期化されていない要素を宣言します。
str DB 'hello',0 ; アドレスstrから始まる6バイトを宣言し、helloとヌル(0)バイトで初期化します。
メモリアドレッシング#
MOV
はメモリとレジスタ間でデータを移動します(デフォルトで 32 ビットデータを移動)。2 つのパラメータを受け取ります:最初のパラメータは目的地、2 番目はソースです。ソースと目的は実際にはアドレスです;[レジスタ] はレジスタの値をアドレスとして参照します;[var] はシンボル var が示すアドレスを参照します;
;合法的なアドレッシングの例:加[]はアドレスを格納していることを示し、参照アドレスはメモリ内容を指します。
mov eax, [ebx] ; EBXに含まれるアドレスのメモリ内の4バイトをEAXに移動します。
mov [var], ebx ; EBXの内容をメモリアドレスvarの4バイトに移動します。(注意、varは32ビット定数です)。
mov eax, [esi-4] ; メモリアドレスESI + (-4)の4バイトをEAXに移動します。
mov [esi+eax], cl ; CLの内容をアドレスESI+EAXのバイトに移動します。
mov edx, [esi+4*ebx] ; アドレスESI+4*EBXのデータの4バイトをEDXに移動します。
;不正なアドレッシングの例:
mov eax, [ebx-ecx] ; レジスタの値を加算することしかできず、減算はできません。
mov [eax+esi+edi], ebx ; アドレス計算には最大で2つのレジスタしか参加できません。
データ型(サイズ)原語(Size Directives)#
ポインタ型を修飾します:データのビット数を示します。
)BYTE PTR
- 1 バイト
)WORD PTR
- 2 バイト
)DWORD PTR
- 4 バイト
mov BYTE PTR [ebx], 2 ; EBXに格納されたアドレスの単一バイトに2を移動します。
mov WORD PTR [ebx], 2 ; EBXのアドレスから始まる2バイトに2の16ビット整数表現を移動します。
mov DWORD PTR [ebx], 2 ; EBXのアドレスから始まる4バイトに2の32ビット整数表現を移動します。
命令#
分類 | 命令 | 説明 | 例 |
---|---|---|---|
データ移動 | mov | ソースオペランドの値をターゲットオペランドにコピーします | mov ax, bx ; bx レジスタの値を ax レジスタにコピーします |
push | データをスタックにプッシュします | push ax ; ax レジスタの値をスタックにプッシュします | |
pop | スタックのトップデータをターゲットオペランドにポップします | pop bx ; スタックのトップの値をポップして bx レジスタに格納します | |
lea | 有効なアドレスをロードし、アドレスをターゲットレジスタに格納します | lea ax, [bx + 4] ; [bx + 4] の有効アドレスを ax に格納します | |
算術 / 論理演算 | add | 加算操作を実行します | add ax, bx ; bx を ax レジスタに加えます |
sub | 減算操作を実行します | sub ax, bx ; ax から bx の値を減算します | |
inc | オペランドに 1 を加えます | inc ax ; ax レジスタの値を 1 増加させます | |
dec | オペランドから 1 を減算します | dec bx ; bx レジスタの値を 1 減少させます | |
imul | 符号付き乗算 | imul ax, bx ; ax = ax * bx 、符号付き乗算 | |
idiv | 符号付き除算 | idiv bx ; ax を bx で除算し、結果を ax と dx に格納します | |
and | ビットごとの AND 操作 | and ax, bx ; ax と bx のビットごとの AND 操作の結果を ax に戻します | |
or | ビットごとの OR 操作 | or ax, bx ; ax と bx のビットごとの OR 操作の結果を ax に戻します | |
xor | ビットごとの XOR 操作 | xor ax, bx ; ax と bx のビットごとの XOR 操作の結果を ax に戻します | |
not | ビットごとの NOT 操作 | not ax ; ax レジスタのすべてのビットを反転させます | |
neg | 負の操作 | neg ax ; ax レジスタの値を反転させます(すなわち、反数を加えます) | |
shl | 左シフト操作 | shl ax, 1 ; ax を 1 ビット左にシフトし、シフトされたビットは破棄されます | |
shr | 右シフト操作 | shr bx, 1 ; bx を 1 ビット右にシフトし、シフトされたビットは破棄されます | |
制御フロー | jmp | 無条件ジャンプ | jmp label ; label タグにジャンプします |
je / jz | 等しい場合にジャンプ(je :equal の場合にジャンプ、jz :zero の場合にジャンプ) | je label ; ゼロフラグが設定されている場合(等しいことを示す)、label にジャンプします | |
jne | 等しくない場合にジャンプ(jump if not equal) | jne label ; ゼロフラグが設定されていない場合(等しくないことを示す)、label にジャンプします | |
jg | 大きい場合にジャンプ(jump if greater) | jg label ; 大きい場合は label にジャンプします | |
jl | 小さい場合にジャンプ(jump if less) | jl label ; 小さい場合は label にジャンプします | |
cmp | 2 つのオペランドを比較します(フラグレジスタを設定します) | cmp ax, bx ; ax と bx の値を比較します(フラグレジスタを設定します) | |
call | サブルーチンを呼び出し、戻りアドレスをスタックにプッシュします | call subroutine ; subroutine サブルーチンを呼び出します | |
ret | プロシージャから戻り、戻りアドレスをポップして呼び出し点にジャンプします | ret ; 現在のプロシージャから呼び出し点に戻ります |
呼び出し規約#
サブルーチン(関数)呼び出しは、一連の共通のプロトコルに従う必要があり、どのように呼び出し、どのようにプロシージャから戻るかを規定します。たとえば、特定の呼び出し規約が与えられると、プログラマーはサブルーチンの定義を確認することなく、どのようにパラメータを渡すかを決定できます。さらに、特定の呼び出し規約が与えられると、高級言語のコンパイラはこれらの規則に従うだけで、アセンブリ関数と高級言語関数を相互に呼び出すことができます。
C 言語の呼び出し規約#
呼び出し規約にはさまざまな種類があります。C 言語の呼び出し規約が最も広く使用されています。この規約に従うことで、アセンブリコードは安全に C/C++ から呼び出され、アセンブリコードから C 関数ライブラリを呼び出すこともできます。
1)ハードウェアスタックのサポートに強く依存します(hardwared-supported stack)
2)push
, pop
, call
, ret
命令に基づいています
3)サブルーチンパラメータはスタックを介して渡されます:レジスタはスタックに保存され、サブルーチンで使用されるローカル変数もスタックに置かれます。
ほとんどのプロセッサで実装されているほとんどの高級手続き型言語は、これに類似した呼び出し慣例を使用しています。呼び出し慣例は 2 つの部分に分かれています。最初の部分は呼び出し元(caller)用、2 番目の部分は呼び出し先(callee)用です。これらの規則を誤って使用すると、スタックが破損し、プログラムがすぐにエラーを起こす可能性があるため、自分のサブルーチンで呼び出し規約を実装する際には特に注意が必要です。
呼び出し元の規則 Caller Rules#
呼び出し元は、サブルーチンを呼び出す前にコンテキストを保存する必要があります:
1)呼び出し元が保存するレジスタcaller-saved registers:EAX, ECX, EDX; これらのレジスタは callee によって変更される可能性があるため、呼び出しが終了した後にスタックの状態を復元するために保存します。
2)サブルーチンに渡すパラメータをスタックにプッシュします:パラメータは逆順にスタックにプッシュされます(最後のパラメータが最初に入ります)。スタックは下に成長するため、最初のパラメータは最も低いアドレスに格納されます(この特性により可変長パラメータリストが可能になります)。
3)call
命令を使用してサブルーチン(関数)を呼び出します:call は自動的に戻りアドレスをスタックにプッシュし、サブルーチンコードの実行を開始します。サブルーチンが戻ると(call が終了した後)、呼び出し先は戻り値を EAX レジスタに格納し、呼び出し元はそこから読み取ることができます。マシンの状態を復元するために、呼び出し元は次のことを行う必要があります:
a.スタックから渡されたパラメータを削除します
b.呼び出し元が保存したレジスタ(EAX
, ECX
, EDX
)を復元します —— スタックからポップします;呼び出し元は、これら 3 つを除いて他のレジスタの値が変更されていないと考えることができます。
;コンテキストを保存
push eax
push ecx
push edx
push [var] ; 最後のパラメータを最初にプッシュ
push 216 ; 2番目のパラメータをプッシュ
push eax ; 最初のパラメータを最後にプッシュ
call _myFunc ; 関数を呼び出します(C命名を仮定);callは自動的に戻りアドレスをスタックにプッシュします。
;コンテキストを復元します。
add esp, 24 ; スタック空間をクリアし、スタックポインタを復元し、渡された6つのパラメータを削除し、スタックポインタを高アドレスに移動します。
mov result, eax ; 戻り値をresult変数に読み取ります。
呼び出し先の規則 Callee Rules#
1)元のスタックフレームベースレジスタ**EBP
**** の値をスタックにプッシュし、次に ****ESP
をEBP
**にコピーします;サブルーチンのスタックフレームの新しいベースとして
2)スタック上にローカル変数のためのスペースを割り当てます;スタックは上から下に成長するため、変数の割り当てに伴い、スタックトップポインタは減少します。
3)呼び出し元が保存したレジスタcallee-saved
—— それらをスタックにプッシュします。これには EBX
, EDI
, ESI
が含まれます;これらのレジスタは呼び出し先が保存および復元する責任があります。
)サブルーチンのコードを実行し、戻り値を**EAX
に保存します;サブルーチンが戻ると:
a. 呼び出し先が保存するべきレジスタ(EDI
, ESI
)を復元します —— スタックからポップします。
b. ローカル変数を解放します
c. 呼び出し元のベースポインタEBP
**を復元します —— スタックからポップします
d. 最後に、ret
**を実行し、呼び出し元に戻ります(caller)
.486 ; アセンブラにどの命令セットアーキテクチャを使用してコンパイルするかを指示します
.MODEL FLAT ; フラットメモリモデル
.CODE ; アセンブリファイル内のコードセクションの開始をマークします
PUBLIC _myFunc ; 外部から見える/リンク可能/このファイルに私有でない関数
_myFunc PROC
;1)
push ebp
mov ebp, esp
;2)
sub esp, 4 ; ローカル変数のためにスペースを割り当てます(4バイト)
;3)
push edi ; レジスタ値を保存します。EDIは変更されます。
push esi ; レジスタ値を保存します。ESIは変更されます。
;)サブルーチン本体
mov eax, [ebp+8] ; 最初のパラメータの値をEAXに移動します。
mov esi, [ebp+12] ; 2番目のパラメータの値をESIに移動します。
mov edi, [ebp+16] ; 3番目のパラメータの値をEDIに移動します。
mov [ebp-4], edi ; EDIをローカル変数に格納します。
add [ebp-4], esi ; ESIをローカル変数に加えます。
add eax, [ebp-4] ; ローカル変数の内容をEAXに加え、最終結果とします。
;)a
pop esi ; ESIのレジスタ値を復元します。
pop edi ; EDIのレジスタ値を復元します。
;)b
mov esp, ebp ; ローカル変数を解放します。
;)c
pop ebp ; 呼び出し元のベースポインタ値を復元します。
;)d
ret ; スタックから戻りアドレスをポップし、戻りアドレスにジャンプして呼び出し元のコードを続行します。
_myFunc ENDP
END ; プログラムの終了をマークします
riscV アセンブリ#
arm アセンブリ#
Intel/AT&T 構文スタイルの違い#
AT&T 構文 | Intel 構文 |
---|---|
insn source, destination | insn destination, source |
メモリ操作#
AT&T メモリアドレッシングは ()
Intel メモリアドレッシングは []
AT&T 構文 | Intel 構文 |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
アドレッシング#
AT&T 構文:disp (base, index, scale)
Intel 構文:[base + index*scale + disp]
最終アドレスは base + disp + index * scale です。
AT&T 構文 | Intel 構文 |
---|---|
movl -12(%rbp), %eax | mov eax, DWORD PTR -12[rbp] |
leaq 0(,%rax,4), %rdx | lea rdx, 0[0+rax*8] |
特定のアセンブリ形式で出力#
objdump 逆アセンブル:Linux 下で objdump はデフォルトで AT&T 形式で逆アセンブルし、-M を追加して出力形式を指定します。
gcc -c test.c // まずgccでバイナリファイルにコンパイルします。
objdump -d test.o // デフォルトでAT&T形式で出力します。
objdump -M intel -d test.o // intel形式で出力を指定できます。
特定のスタイルのアセンブリコンパイラを選択して、アセンブリファイルを生成して確認することもできます。
gcc -S test.c // デフォルトでAT&T形式で出力します。
gcc -S -masm=intel test.c // intelの構文形式で出力します。