8086系アセンブラ - NASM

機械語の特色

機械語は、CPUの構造に直接語り掛ける文法となっている。 プログラムはどれも計算機の主記憶上で動く。 CPUからみると、主記憶は連番のアドレスが振られた格納場所にすぎないが、 実行中のプログラムが途中で書き換えられてはならないため、 プログラムを格納するセグメントとデータを格納するセグメント、など 複数のものに分ける。

機械語そのものは数値にすぎない。その働きに名前をつけたものが ニーモニックで純粋な機械語はここまで。 プログラミングするために、アドレスや定数に名前をつけて マクロ利用できるような機能を持ったものがアセンブリ言語である。

さらに、生成された機械語をOSが正しく実行するために決められた バイナリデータの置き方、関数(サブルーチン)呼出しの規約を決めたものが ABI(Application Binary Interface)で、 以上を総合してアセンブラプログラミングとなる。

インストラクションとオペランド

機械語命令に1対1対応するものがインストラクションで、 それに与える引数をオペランドという。インストラクションを 人間の分かる単語に置き換えたものをニーモニックという(例: MOV)。

擬似命令

機械語ではなくアセンブラ処理に必要なマクロなどの擬似命令がある。

擬似命令用途
EQU 定数マクロ定義
DB, DW, DQ バイト, ワード、Qワードデータ配置

アラインメント(alignment)

メモリアクセスは 2N 切りの良いところで行なわれる。 2, 4, 8, 16バイト の境界に合わせることが必要。

エンディアン(endian)

たとえば0x12を8bit、16bitでそれぞれ表現すると。

0001 0020 |
0000 0000 | 0001 0020
同じアドレスに8bitマシンでアクセスすると前者は 0x12、後者は 0x00 に見える。

16bitで表現された数値を8bitマシンでも読めるようにしよう

と考えると、上位桁をあとで書くと可能となる。

0001 0020 |
0001 0020 | 0000 0000
8bitマシンで同じアクセスするといずれも 0x12 になる。 8bit単位で、下位桁を先にメモリに格納する方法を リトルエンディアン(little endian) という。 上位桁を先に格納するのを ビッグエンディアン(big endian) という。CPUアーキテクチャによって異なり Intel/AMD アーキテクチャはリトルエンディアン。

レジスタ/メモリ/スタック

変数のように使えるものがレジスタとメモリのみ。

レジスタ

CPUに内蔵された値格納・計算場所で最速。

メモリ

主記憶。CPUからはアドレスを表すレジスタ経由でアクセスする。 メモリ同士で計算したりすることはできず、メモリにある2つの 値の演算をしたい場合は片方をレジスタに読み込んでから行なう。

AMD64アーキテクチャのレジスタは基本が64ビットで一覧は以下のとおり。

R0  R1  R2  R3  R4  R5  R6  R7  R8  R9  R10  R11  R12  R13  R14  R15
RAX RCX RDX RBX RSP RBP RSI RDI
EAX ECX EDX EBX ESP EBP ESI EDI
AX  CX  DX  BX  SP  BP  SI  DI
AL  CL  DL  BL  SPL BPL SIL DIL

16ビット時代からの歴史的経緯で最初の8つは各々2行目の名前で利用でき、 そちらが利用されることが多い。3行目は同じレジスタの 下位32ビットだけを使うときの名前、 4行目は同じく下位16ビット、 5行目は同じく下位8ビットを使うときの名前である。

レジスタは数が限られているがレジスタを介さないとできない演算があるの で、現在のレジスタ地をスタックに待避、スタックから復帰する命令がある。

push レジスタ
pop レジスタ

とすると汎用レジスタの待避と復帰が行なえ、

pushf
popf

とするとフラグレジスタの待避と復帰が行なえる。

ラベルとメモリ参照

アセンブラリスト中、

LABEL:	

とコロン後置したものはラベルとして扱われその地点のアドレスが 自動的に入る。このアドレス自体の値は LABEL で、 アドレスに格納されている値は [LABEL] で得られる。

	section .bss
hello		db	"Hello, world!", 0
hellolen	equ	$-hello		; $は現在地アドレス
	section .text
	mov	si, hello		; hello のアドレス
	mov	cl, byte [hello]	; hello番地にあるバイトデータ値

フラグ

フラグ働き
CF(キャリーフラグ) 算術で繰り上がり/繰り下がりが起きたときに1
ZF(ゼロフラグ) 算術結果がゼロだったときに1
SF(サインフラグ) 算術結果が負だったときに1
OF(オーバーフラグ) 桁溢れのときに1

フラグは加減乗除算、インクリメント、デクリメント等 算術演算の起こる命令後に操作される。 現状のフラグを保存したいときは pushf/popf 命令を用いる。

ジャンプ命令

命令 働き
jmp 無条件ジャンプ
jc, jnc キャリーが立っていたら(いなかったら)ジャンプ
jz, jnz ゼロフラグが立っていたら(いなかったら)ジャンプ
js, jns サインフラグが立っていたら(いなかったら)ジャンプ
jo, jno オーバーフラグが立っていたら(いなかったら)ジャンプ

システムコール

OSの具備している処理は システムコール と呼ばれ、 機能ごとに番号が定義されていて syscall 命令で呼ぶ。 /usr/include/sys/syscall.h に対応表がある。

AMD64アーキテクチャのOSではシステムコール呼出しの引数指定に レジスタを使う。

レジスタ代入する値
EAXシステムコール番号
RDI第1引数
RSI第2引数
RDX第3引数
R10第4引数 (一般関数の場合は R10のかわりに RCXを使う)
R8 第5引数
R9 第6引数
RAX返却値が入って帰って来る

ディスアセンブル

objdump -M intel -D objectFile

で逆アセンブルされる。

主なインストラクション

インストラクション意味
MOV DEST, SRC DESTSRC を代入する
CMP OP1, OP2 OP1OP2 を比較し結果をフラグに反映させる
OR OP1, OP2 OP1OP2 のビットORを取り結果をOP1に入れフラグに反映させる
AND OP1, OP2 OP1OP2 のビットANDを取り結果をOP1に入れフラグに反映させる
XOR OP1, OP2 OP1OP2 のビットXORを取り結果をOP1に入れフラグに反映させる
ADD OP1, OP2 OP1OP2 を加算し結果をOP1に入れフラグに反映させる
SUB OP1, OP2 OP1 から OP2 を減算し結果をOP1に入れフラグに反映させる
MUL OP1 AX と OP1 を乗算し結果を (RDX:RAX)の128ビットに入れフラグに反映させる
DIV OP1 (RDX:RAX)の128ビットをOP1 で除算し商をAX, 剰余をDXに入れフラグに反映させる
INC OP1 OP1を1減らす
DEC OP1 OP1を1増やす
LOOP LABEL RCXから1減らしゼロでなければ LABELにジャンプする
LOOP LABEL RCXから1減らしゼロでなければ LABELにジャンプする
SYSCALL OSのシステムコールを呼ぶ(上記参照)
CALL LABEL サブルーチン(関数) LABEL を呼ぶ
RET サブルーチンから戻る

CMPやADD/SUB/MUL/DIVなどの演算結果とそれに応じた ジャンプ命令を組み合わせてプログラムを記述する。

Hello, world

hello.asm

これを改良する。テーマをいくつか挙げる。

  1. helloラベルからの文字を1字ずつ出力する(ループさせる)
  2. 10進数を指定してその10進文字列を出力する
  3. 10進数を指定してその16進文字列を出力する

リンク集