アセンブラ (nasm)
nasm のインストール
アセンブリ言語でプログラミングするためにはアセンブラが必要です。Linux 上で動作するアセンブラとしては GNU as(gas) と NASM が代表的です。 gas と NASMではソースの記述方法が異なり、gas は AT&T表記、NASM は Intel表記 となっています。AMDもインテルのマニュアルもIntel表記のため、nasm を使用する ことにします。NASM は https://nasm.sourceforge.net で配布されていますが、Ubuntu の場合ならメニューのシステム/システム管理/Synaptic パッケージマネージャ から「nasm」をインストールするか、アプリケーション/アクセサリ/端末 を起動して以下のコマンドでインストールしてください。
sudo apt-get install nasm
Ubuntu 8.04 には 0.99.06 がインストールされ、Ubuntu 8.10 では 2.03.01、Ubuntu 9.04 では現在の最新版 nasm-2.05.01 がインストールされます。0.99以降のバージョンならば x86-64 がサポートされています。
nasm の使い方
アセンブリ言語で書いたソース(myfile.asmとします)をアセンブルするには、 以下のコマンドを入力します。
nasm -f elf64 myfile.asm
ソースファイルの拡張子は自由な名称で構いません。出力されるオブジェクト ファイルのファイル名はソースファイルの拡張子が o に変更されたファイル名 (ここでは myfile.o)が生成されます。「-f elf64」は出力するファイル形式を 指定するオプションで、64bit Linux の場合は「elf64」を指定する必要があります。 空白を除いて「-felf64」でも問題ありません。
出力されるオブジェクトファイルの名前を指定する場合は以下のように「-o」 に続けて出力ファイル名を指定します。あいだの空白はなくても構いません。
nasm -f elf64 myfile.asm -o MyFile.obj
もうひとつ便利なオプションがあります。「-l myfile.list」など「-l」に続けてファイル名を入力するとアセンブル結果のリストが出力されます。
nasm -f elf64 myfile.asm -l myfile.list
オブジェクトファイルはリンクして実行形式に変換する必要があります。
ld -s -o myfile myfile.o
リンカ(ld)の出力ファイル名も「-o」に続けて出力ファイル名を指定します。 あいだの空白はなくても構いません。
次の様にシェルスクリプトを書けば楽になります。
#!/bin/sh nasm -f elf64 $1.asm -l $1.lst && ld -s -o $1 $1.o
このシェルスクリプトのファイル名を asm とした場合、 chmod +x asm として実行属性を設定しておきます。
これで asm myprog とすれば、myprog.asm が myprog という実行形式にアセンブルされます。リストが myprog.lst に出力されます。
実行して確認するには ./myprog と入力します。
Makefile を作成するまでもないでしょう。
アセンブリソース
nasm で書いたソースファイルを示します。"hello, world" を端末に表示するだけの プログラムです。セミコロン(;)から行末まではコメントになります。
;------------------------------------ ; hello.s ; nasm -f elf64 hello.s ; ld -o hello01 hello.o ; ./hello ;------------------------------------ bits 64 section .text global _start _start: xor eax, eax mov edx, eax inc eax ; sys_write (01) mov edi, eax ; stdout (01) mov dl, len ; length (13) mov rsi, msg ; address syscall xor edi, edi ; return 0 mov eax, edi mov al, 60 ; sys_exit syscall section .data msg db 'hello, world', 0x0A len equ $ - msg
bits 64 行は64bitモードの指定、section .text行はELF形式の実行ファイルのプログラムを配置する .text セクションの指定、global _start の行はプログラムが実行を開始する場所の指定です。アセンブリソースの先頭部分に以上の3行を置きます。_start:はラベルでここからプログラムの実行が開始されます。section .dataの行以降は初期化が必要なデータ用のセクションで実行ファイルに含まれます。このソースでは使っていませんが、section .bssは初期化が不要なデータ用の領域で実行ファイルには含まれません。
リストファイルの内容は以下のようになります。
1 ;------------------------------------ 2 ; hello.s 3 ; nasm -f elf64 hello.s 4 ; ld -o hello01 hello.o 5 ; ./hello 6 ;------------------------------------ 7 8 bits 64 9 section .text 10 global _start 11 12 _start: 13 00000000 31C0 xor eax, eax 14 00000002 89C2 mov edx, eax 15 00000004 FFC0 inc eax ; sys_write (01) 16 00000006 89C7 mov edi, eax ; stdout (01) 17 00000008 B20D mov dl, len ; length (13) 18 0000000A 48BE- mov rsi, msg ; address 19 0000000C [0000000000000000] 20 00000014 0F05 syscall 21 00000016 31FF xor edi, edi ; return 0 22 00000018 89F8 mov eax, edi 23 0000001A B03C mov al, 60 ; sys_exit 24 0000001C 0F05 syscall 25 26 section .data 27 00000000 68656C6C6F2C20776F- msg db 'hello, world', 0x0A 28 00000009 726C640A 29 len equ $ - msg
青字の部分 がアセンブラが生成した x86-64 CPU が実行するバイナリコードになります。
NASM の特徴
nasm は GNU as より, DOS系のアセンブラに近く、MASM, TASM より単純なため 任意の1行を見れば、アセンブルされるマシン語がわかるようになっています。 強力なマクロ機能もあり、抽象化したコードの作成も可能です。
例えば次の例ではラベルの宣言が異なりますが、
foo equ 1 bar dw 2 mov ax, foo mov ax, bar
この場合は ax には定数値が代入され、
move ax, [foo] move ax, [bar]
この例では、ax には メモリの内容が代入されます。 つまり、メモリ参照時には必ず[角カッコ]で囲む必要があり、[foo] となって いれば foo の宣言が何であってもメモリ参照であることが確定し、 foo であれば定数として扱われます。
オペランドの記述(アドレッシングモード)も違いがあります。
MASM | NASM |
---|---|
mov ax, table[bx] | mov ax, [table + bx] |
mov ax, es:[di] | mov ax, [es:di] |
また、NASM では宣言時の変数の型を保持しません。したがってサイズが あいまいな場合(メモリへの格納など)には明示的にサイズを指定します。 var dw 0 と宣言されている場合でも、mov var, 2 ではなく、 mov word [var], 2 とサイズを明記する必要があります。 同様に LODS, MOVS, STOS 等のサイズ指定の無い命令は使用できず、 LODSB, MOVSW, SCASD のようにサイズ指定された命令を使用します。
MASM | NASM |
---|---|
TBYTE | TWORD |
ST(0), ST(1) | st0, st1 |
stack db 64 dup (?) | stack resb 64 (dup は未サポート) |
マクロを除くと基本的に1行だけで生成されるコードが決まるように 文法が単純になっています。
マニュアルページ (man nasm) の文法に関する部分を訳したものを示します。 より詳細な解説はドキュメントを参照して下さい。
SYNTAX この man page は nasm のアセンブリ言語の文法を完全に記述し ているわけではなく、他のアセンブラとの差の要旨を示している。 gas と異なりレジスタ名の前の`%' は不要であり, 浮動小数点ス タックレジスタは st0, st1 等と参照される。 浮動小数点命令は1オペランド形式と2オペランド形式を使用で きる。 そのため TO キーワードが用意されている。 したがって、以下の様に記述するしても良いし、 fadd st0,st1 fadd st1,st0 また、1オペランド形式を使用することもできる。 fadd st1 fadd to st1 初期化されないメモリの確保には RESB, RESW, RESD, RESQ, REST 擬似コードを用いる。 それぞれ1つのパラメータでバイト、 ワード、ダブルワード、4倍ワード、10バイトワードを単位として 確保する数を指定する。 データの繰り返しを DUP で指定するDOS用のアセンブラと異なって 次のように TIMES プレフィックスを用いる。 message: times 3 db 'abc' times 64-$+message db 0 文字列 `abcabcabc' と全体の長さが64バイトになるように0がに続 くデータが設定される。 シンボルの参照は常にイミディエイト(すなわちシンボルのアドレス) と解釈される。 ただし角括弧を使うとそのアドレスのメモリの内容 を示す。 mov ax,wordvar これは、変数 wordvar のアドレスがAXに設定される。 mov ax,[wordvar] mov ax,[wordvar+1] mov ax,[es:wordvar+bx] これらのどの形式でもメモリの内容が参照される。 以下の形式は使用できない。 mov ax,es:wordvar[bx] es mov ax,wordvar[1] ただし、セグメントレジスタ名を命令のプリフィックスとして用 いることはでき、他の方法でオーバーライドできない LODSB の ような命令に使われる。 定数はほとんどの数値形式で表現できる。つまり H, Q, B を後ろ に付けて、16進数, 8進数、2進数としたり、0x か $ を前において 16進数を表すことができる。 文字定数はシングルクォートかダブルクォートで囲み、エスケープ 文字はない。 並びはリトルエンディアン(逆順)であり、'abcd' と いう文字定数は 0x64636261 であって 0x61626364 ではない. ローカルラベルは名前がピリオドから始まるラベルであり、局所 性は直前のローカルでないラベル以降となる。つまり `label' と いうラベルの後ろで `.loop' というローカルラベルを宣言した 場合、実際には `label.loop' と宣言されたものとして扱われる. DIRECTIVES 「SECTION 名前」または 「SEGMENT 名前」 はすべての後に続く コードは名前のつけられたセクションに属するものとして name に指定することになる。 セクション名は出力ファイルの形式に依存するが、ほとんどの形 式で .text, .data, .bss をサポートしている. (例外は obj 形式で, すべてのセグメント名はユーザ定義可能である.) 「ABSOLUTE アドレス」は概念的なアセンブリ位置をその絶対アド レスに指定する。コードもデータも生成されないが、RESB, RESW, RESD を使ってアセンブリ位置を進めることができ、ラベルを定義 することができる。この擬似命令をデータ構造の定義に使 用することができる。絶対アドレスの指定を終わらせるために、 別のセクションディレクティブを指定して通常の状態に戻さなけ ればならない。 BITS 16 と BITS 32 またはBITS 64 は nasm が生成するプロセッサ のデフォルトモードを切り替える。つまりDOS用のアセンブラの USE16とUSE32に相当する. 「EXTERN シンボル名」と 「GLOBAL シンボル名」はシンボル定義 をそれぞれ別のモジュールからインポートするか、別モジュールに エクスポートする。 GLOBAL ディレクティブはシンボルが定義され る前に置く必要があることに注意すること。 「STRUC strucname」と ENDSTRUC をいくつかの RESB, RESW など の命令を囲むように使うとデータ構造を定義できる。 構造体メンバのオフセットを定義することに加えて、この構造は 構造体名に _size を付加した構造体の大きさのシンボルを定義 する。