NASM

NASM
NASM (Netwide Assembler) は、80x86 および x86-64 用のアセンブラです。

アセンブラには、AT&T 記法と Intel 記法の2つがあります。
NASM では Intel 記法が使われ、GNU assembler (gas) では AT&T 記法が使われます。

アーキテクチャの仕様書では Intel 記法が使われており、感覚的にもこちらの方が使いやすいので、NASM を使うことにします。

対応 OS
NASM は、Unix/Windows/MacOS X といった、各 OS 用のオブジェクトフォーマットに対応しているので、複数の OS に対応することもできます。

ただし、アセンブラコードは、各 OS の仕様などに対応するように記述しなければならないため、一つのコードで複数の OS に対応するには、マクロなどを使ってコードを分けたりする必要があります。
使い方
$ nasm -f <format> <filename> [-o <outfile>]

nasm コマンドを使って、アセンブラのソースファイル (*.asm) をコンパイルし、-f オプションで指定されたフォーマットで、 オブジェクトファイルを出力します。
※実行ファイルを生成するためには、別途リンカを使う必要があります。

-o オプションで出力ファイル名の指定がない場合は、ソースファイル名から自動で名前を付けます。
出力フォーマットに合わせて、拡張子を自動で置き換えます (.o や .obj など)。

-f オプションでの出力フォーマット指定は、基本的に常に必要です。
デフォルトの出力形式は bin になっており、この場合、特定のフォーマットに格納する形ではなく、生のバイナリだけを出力します。

他のオプションに関しては、gcc と似たようなものが多いです。
基本的なオプション一覧
以下は、基本的なオプションの一覧です。

基本的に、オプションと引数の間は、空白を入れても入れなくても動作します。

-h, --helpヘルプの表示
-o <filename>出力ファイル名の指定。
省略された場合、ソースファイル名から自動で名前を付ける。
-f <str>出力フォーマットの指定。
デフォルトは bin
-l <filename>ソースリストファイルを生成。
このファイルには、アドレス・生成されたバイナリ数値・元のソースコードが出力されます。
アセンブラコードに対応するバイナリを確認したい時に使います。
-O<flags>最適化の指定。複数のフラグを指定できます。

-O0 : 最適化なし。
-O1 : 最小限の最適化。ジャンプ命令のオフセット値の最適化を行います。
-Ox : マルチパスの最適化 (デフォルト)
コードで strict キーワードが使用されていない限り、ジャンプ命令のオフセットと、符号付きの即値バイトのサイズを最適化して、バイナリのサイズを減らします。
x の文字の代わりに、"2" 以上の数字を指定することもできます。
-Ov : アセンブリの最後に、実際に実行された最適化パスの数を出力します。

即値のサイズの調整や、オペランドサイズの調整といった最適化は行われますが、命令自体を置き換えるような最適化は行われません。
-g出力ファイルにデバッグ情報を付加する。

-F でデバッグの出力形式が指定されていない場合、-f で指定された各出力形式ごとのデフォルトが使われます。
Unix/MacOS X 用なら、dwarf になります。
-i/-I <path>インクルードファイルの検索ディレクトリの指定。
ver 2.14 より前では、最後にパス区切り (/) が必要です。
-d/-D <macro[=VAL]>マクロの定義
-u/-U <macro>マクロの定義を解除
-MD <file>Makefile 形式でのファイル依存関係を、ファイルに出力する
出力フォーマット
bin生のバイナリ (default)
elf32ELF32 (i386)。Unix 用 32bit
elf64ELF64 (x86-64)。Unix 用 64bit
elfx32ELFx32。x86-64 上の 32bit
win32Microsoft extended COFF for Win32
win64Microsoft extended COFF for Win64
macho32Mach-O i386 (MacOS X)
macho64Mach-O x86-64 (MacOS X)
elfelf32 と同じ
winwin32 と同じ
machomacho32 と同じ
サンプルコード
とりあえず、C 言語から、アセンブラで記述された関数を呼び出す形の、簡単なサンプルを実行してみます。

<02_test.asm>
global testfunc

section .text

testfunc:
    mov eax, 123
    ret

<test1.c>
#include <stdio.h>

int testfunc(void);

int main(void)
{
    int a = testfunc();

    printf("%d\n", a);

    return 0;
}

# 02_test.o を出力
$ nasm -f elf64 02_test.asm

# C コンパイル
$ cc -o test test1.c 02_test.o

$ ./test
123
解説
アセンブラでは、testfunc という名前の関数を定義し、その関数を C 言語から呼び出します。

testfunc 関数は、EAX レジスタに 123 (10進数) の値を入れて、関数から戻るだけのシンプルなコードです。

C 言語の方では、02_test.o 内で定義された testfunc 関数を呼び出し、int 型の戻り値を取得して、printf で表示しています。
関数からの整数の戻り値には、RAX レジスタが使われます。
結果として、testfunc 関数の戻り値は、123 になります。

EAX レジスタは 32bit サイズ、RAX レジスタは 64bit サイズのレジスタです。
(RAX と EAX は同じレジスタで、名前によって、使用するサイズが異なるだけです)

C 言語で宣言されている testfunc 関数の戻り値は int 型なので、実際には、RAX レジスタに格納されている値の、下位 32bit のみが数値として使われます。
コードの詳細
  • global testfunc
    testfunc という名前のシンボルを、他のモジュールから参照できるように、グローバル宣言しています。
    これがないと、C 言語の方から、testfunc という名前を関数として認識できません。
  • section .text
    詳細は後述しますが、これ以降に出力されるバイナリは、出力フォーマットの .text セクションに、プログラムコードとして配置されます。
  • testfunc:
    アドレス位置を参照するためのラベルですが、このシンボル名は、関数名としても使われます。
  • ret
    関数から戻る場合、RET 命令を使います。

C 言語の方では、testfunc 関数を呼び出せるようにするため、アセンブラで定義された関数の引数と戻り値と一致するように、プロトタイプ宣言します。
引数はないので void。戻り値は int 型です。

戻り値は、一つの整数またはポインタの値を返す場合、関数が戻った時の RAX レジスタの値が使われます。
C 言語では、明確に戻り値のサイズを指定する必要があるため、実際には、RAX レジスタ内の、指定サイズの範囲の値のみが使われます。
逆アセンブル
testfunc 関数内のバイナリコードが、実際にどのように出力されているかを確認するため、逆アセンブルしてみます。

objdump コマンドを使うと、実行ファイルやオブジェクトファイルの内容を出力することができ、逆アセンブルも行えます。
objdump コマンドは、binutils パッケージに含まれています。

$ objdump -d -M intel 02_test.o

02_test.o:     ファイル形式 elf64-x86-64

セクション .text の逆アセンブル:

0000000000000000 <testfunc>:
   0:    b8 7b 00 00 00           mov    eax,0x7b
   5:    c3                       ret

-d, --disassembleすべてのプログラムコードのセクションを逆アセンブルする
-M, --disassembler-options=OPT逆アセンブルのオプション指定。
デフォルトで AT&T 記法になるので、
intel を指定して、Intel 記法にすることができます。

mov eax,0x7b (123) は、「b8 7b 00 00 00」の 5 byte として、バイナリにエンコードされています。

0x7b の即値 (直接の数値) は、値を格納する先のレジスタが EAX で、32bit サイズであることから、格納するソース値も 32bit 数値で指定する必要があるため、「7b 00 00 00」として、4 byte の値が、リトルエンディアンで出力されています。

ret は、「c3」の 1 byte でエンコードされています。