ラザニア

ラザニア

学習演習

はじめに

基本

コメント

Exercismのx86-64トラックでは、アセンブラーとしてNASM(The Netwide Assembler)を使います。 NASMでは、コメントの先頭にはセミコロン(;)を付けます。

コメントはプログラムのどこにでも書くことができ、;より後ろはすべてアセンブラーに無視されます。

定数

NASMでは、equを使ってアセンブラー時に評価される定数を定義できます。 たとえば、これはUNIVERSEという名前の定数を値42で定義します:

UNIVERSE equ 42

定数は定義されたときに一度だけ評価され、あとから再定義することは_できません_。

汎用レジスタ(GPR)

アセンブリでは、変数の代わりに、ふつうはレジスタを使って値を保存します。 CPUレジスタは、コンピューターのプロセッサー内部にある高速なメモリーです。 ほとんどの計算は、CPUのレジスタで行われます。

レジスタの一部は、さまざまな計算に使われ、**汎用レジスタ(GPR)**と呼ばれます。 それ以外のレジスタには、特別な、あるいは専用の役割があります。

x86-64には16個の64ビット汎用レジスタ(GPR)があり、32ビット、16ビット、8ビットとしてもアクセスできます。 汎用レジスタは以下のとおりです。ここでrnのnは8から15の範囲です:r8、r9、r10、r11、r12、r13、r14、r15。

64ビット 32ビット 16ビット 8ビット
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。

Caution

この先シラバスの中で、レジスタを保存する方法を学びます。 今のところは、2つ目の種類のレジスタだけを使いましょう:rax、rcx、rdx、rdi、rsi、r8、r9、r10、r11。

命令

命令とは、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に置きます。

関数の宣言は次のもので構成されます:

  1. 関数名を付けたラベルと、それに続く:。
  2. 関数を定義する命令。
  3. リターン命令のret。

このトラックでは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

説明

この演習では、お気に入りの料理本に載っている最高のラザニアを作るために、コードを書いていきます。

取り組むタスクは4つあり、どれもラザニアを調理するのにかかる時間に関するものです。

Note

このコンセプトで説明されている命令は次のとおりです:

命令 説明
mov a, b b の内容を a にコピーします
add a, b a = a + b
sub a, b a = a - b
imul a, b a = a * b
call a 関数 a を呼び出します
ret 関数から戻ります

1. オーブンでの想定加熱時間を分で定義する

引数を一切取らず、ラザニアをオーブンに入れておくべき時間を分で返すexpected_minutes_in_oven関数を定義します。 料理本によると、想定されるオーブン加熱時間は40分です。

expected_minutes_in_oven();
// => 40

2. 残りのオーブン加熱時間を分で計算する

ラザニアが実際にオーブンに入っていた分数を仮引数として受け取り、前のタスクで求めた想定オーブン加熱時間をもとに、ラザニアをあと何分オーブンに入れておく必要があるかを返すremaining_minutes_in_oven関数を定義します。

remaining_minutes_in_oven(30);
// => 10

3. 準備時間を分で計算する

ラザニアに重ねた層の数を仮引数として受け取り、1層につき2分かかるものとして、ラザニアの準備に何分かかったかを返すpreparation_time_in_minutes関数を定義します。

preparation_time_in_minutes(2);
// => 4

4. 経過時間を分で計算する

次の順番で2つの仮引数を受け取るelapsed_time_in_minutes関数を定義します:

  1. ラザニアに重ねた層の数。
  2. ラザニアをオーブンに入れておいた分数。

この関数は、ラザニアの調理に何分かかったかを返す必要があります。 これは、準備にかかった分数と、その時点でラザニアをオーブンに入れておいた分数の合計です。

elapsed_time_in_minutes(3, 20);
// => 26
GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
x86-64 Assembly Exercism

ラザニアを始める準備はできましたか?

Exercismに登録すれば、22個のコンセプト130個の演習、そして本物の人間によるメンタリングとともに、x86-64 Assemblyを学んでマスターできます。すべて無料です。