Exercismのx86-64トラックでは、アセンブラーとしてNASM(The Netwide Assembler)を使います。
NASMでは、コメントの先頭にセミコロン(;)を付けます。
コメントはプログラムのどこにでも書くことができ、;より後ろはアセンブラーによって無視されます。
NASMでは、equを使ってアセンブル時の定数を定義できます。
たとえば、次のコードは42という値を持つUNIVERSEという名前の定数を定義します:
UNIVERSE equ 42
定数は定義されたときに一度だけ評価され、後から再定義することは_できません_。
アセンブリでは、変数の代わりに、値を格納するためにレジスタをよく使います。 CPUレジスタは、コンピューターのプロセッサー内部にある高速なメモリーです。 ほとんどの計算は、CPUのレジスタで行われます。
一部のレジスタはさまざまな計算に使われ、**汎用レジスタ(GPRs)**と呼ばれます。 それ以外のレジスタには、特別な目的や専用の目的があります。
x86-64には16個の64ビット汎用レジスタ(GPRs)があり、32ビット、16ビット、8ビットとしてもアクセスできます。
汎用レジスタを以下に示します。ここでrnのnは8から15の範囲です: r8、r9、r10、r11、r12、r13、r14、r15。
| 64-bit | 32-bit | 16-bit | 8-bit |
|---|---|---|---|
| rax | eax | ax | ah/al |
| rbx | ebx | bx | bh/bl |
| rcx | ecx | cx | ch/cl |
| rdx | edx | dx | dh/dl |
| rsi | esi | si | sil |
| rdi | edi | di | dil |
| rbp | ebp | bp | bpl |
| rsp | esp | sp | spl |
| rn | rnd | rnw | rnb |
64ビット未満を使う場合、アクセスされるビットは通常、レジスタの下位部分です。
この規則の例外はah、bh、ch、dhで、これらはレジスタの16ビット部分の上位8ビットにアクセスします。
raxレジスタでビットにアクセスする方法の図:
+--------+---------------------------------------+
| 64-bit | rax |
+--------+-------------------+-------------------+
| 32-bit | | eax |
+--------+-------------------+---------+---------+
| 16-bit | | ax |
+--------+-----------------------------+----+----+
| 8-bit | | ah | al |
+--------+-----------------------------+----+----+
これらのレジスタのいくつかは、関数呼び出しをまたいで保存しておかなければなりません: rbp、rsp、rbx、r12、r13、r14、r15。
これらを保存しないと、エラーや未定義の動作につながることがあります。
それ以外のレジスタは保存されないため、自由に使えます: rax、rcx、rdx、rdi、rsi、r8、r9、r10、r11。
レジスタは通常、push命令を使ってスタックに保存します。
そして関数の終わりに、pop命令で元に戻します。
命令は、CPUが実行できる計算の単位です。 命令は通常、次のような形をしています:
name destination, source
つまり、最初に命令の名前を書き、次に1つ以上の空白、続いて宛先オペランド、コンマ(,)、そして最後にソースオペランドを書きます。
通常、命令によって変更されるのは宛先オペランドだけで、ソースオペランドは変更されません。
特に断りのない限り、両方のオペランドは同じサイズでなければなりません。 たとえば、ソースオペランドが16ビットなら、宛先オペランドも16ビットでなければなりません。
たとえば、レジスタに値を格納するには、mov命令を使います:
mov rax, rdx ; rax = rdx
; mov is the name, rax is the destination operand and rdx is the source operand
上のコードは、ソースオペランドであるrdxの64ビットすべての内容を、この命令の宛先オペランドであるraxの64ビットすべてにコピーします。
ほとんどの命令では、ソースオペランドとして整数の定数を使えます(宛先オペランドとしては使えません):
mov rcx, 42 ; rcx = 42
; rcx is the destination operand and 42 is the source operand
32ビットレジスタに書き込むと上位ビットもクリアされるため、mov eax, 42はmov rax, 42と同じであることに注意してください。
これは16ビットレジスタと8ビットレジスタには当てはまりません。
加算、減算、乗算の算術演算には、add、sub、imul命令を使います:
add rax, rsi ; rax = rax + rsi
imul rax, rdi ; rax = rax * rdi
sub rax, r8 ; rax = rax - r8
命令は関数の中にまとめます。 すべての関数は、ソースファイルのsection .textに置きます。
関数の宣言は、次の要素で構成されます:
:。関数に引数を渡す方法と、関数から値を返す方法には、特定の取り決めがあります。 主な呼び出し規約には、System V AMD64 ABIとMicrosoft x64の2つがあります。
このトラックではSystem V AMD64 ABI呼び出し規約を使い、最初の6つの整数引数はレジスタで関数に渡されます。
引数は次の順序で渡されます: rdi、rsi、rdx、rcx、r8、r9。
整数の戻り値は、raxレジスタで関数から返されます。
関数を呼び出すには、call命令を使います。
関数は、同じソースファイル内のどこからでも呼び出せます。
関数を他のソースファイルから見えるようにするには、globalディレクティブを使う必要があります。
たとえば、次のコードはsumという関数を宣言します:
section .text ; functions are placed here
global sum ; sum is visible to other source files
sum:
; first argument is passed in rdi
; second argument is passed in rsi
; return value is placed in rax
mov rax, rdi ; rax is now equal to rdi
add rax, rsi ; rax = rax + rsi
ret ; function return
そして次のコードは、引数3と5でsum関数を呼び出します:
mov rdi, 3 ; First argument in rdi
mov rsi, 5 ; Second argument in rsi
call sum
; The rax register now contains the value 8 (3 + 5), after sum returns