Exercism 的 x86-64 軌道使用 NASM(The Netwide Assembler)作為組譯器。
在 NASM 中,註解以分號(;)開頭。
註解可以放在程式中的任何位置,而;之後的所有內容都會被組譯器忽略。
在 NASM 中,可以使用equ定義組譯時期的常數。
例如,以下程式碼定義了一個名為 UNIVERSE、值為 42 的常數:
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。
在課程大綱的後續內容中,你會學到如何保留暫存器。
目前請只使用第二類暫存器:rax、rcx、rdx、rdi、rsi、r8、r9、r10 和 r11。
指令是 CPU 可以執行的運算單位。 它們通常具有以下形式:
name destination, source
也就是說,指令名稱放在最前面,接著至少一個空白字元,然後是目的運算元、一個逗號(,),最後是來源運算元。
指令通常不會修改來源運算元,只會修改目的運算元。
除非另有說明,否則兩個運算元的大小必須相同。 例如,如果來源運算元是 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 中。
函式宣告包含:
:。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
在這道練習中,你會撰寫一些程式碼,幫助你依照最愛的食譜做出美味的千層麵。
你有四個任務,全都和烹調千層麵所花費的時間有關。
這些是這個概念中提到的指令:
| 指令 | 說明 |
|---|---|
| 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 | 從函式回傳 |
定義expected_minutes_in_oven函式,這個函式不接受任何參數,並回傳千層麵應該在烤箱中加熱幾分鐘。
根據食譜,預期的烤箱時間為 40 分鐘:
expected_minutes_in_oven();
// => 40
定義remaining_minutes_in_oven函式,它會接受千層麵實際已在烤箱中的分鐘數作為參數,並根據上一個任務中預期的烤箱時間,回傳千層麵還需要在烤箱中待幾分鐘。
remaining_minutes_in_oven(30);
// => 10
定義preparation_time_in_minutes函式,它會接受你加到千層麵的層數作為參數,並回傳你準備千層麵所花的分鐘數,假設每一層需要花你 2 分鐘準備。
preparation_time_in_minutes(2);
// => 4
定義elapsed_time_in_minutes函式,這個函式接受兩個參數,順序如下:
這個函式應該回傳你花了幾分鐘烹調千層麵。 這是準備時間(分鐘)和千層麵目前為止在烤箱中花費的時間(分鐘)的總和。
elapsed_time_in_minutes(3, 20);
// => 26