千層麵

千層麵

學習練習

簡介

基礎

註解

Exercism 的 x86-64 軌道使用 NASM(The Netwide Assembler)作為組譯器。 在 NASM 中,註解以分號(;)開頭。

註解可以放在程式中的任何位置,而;之後的所有內容都會被組譯器忽略。

常數

在 NASM 中,可以使用equ定義組譯時期的常數。 例如,以下程式碼定義了一個名為 UNIVERSE、值為 42 的常數:

UNIVERSE equ 42

常數只在定義時求值一次,之後就_不能_重新定義。

通用暫存器(GPRs)

在組合語言中,我們通常使用暫存器來儲存值,而不是變數。 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。

Caution

在課程大綱的後續內容中,你會學到如何保留暫存器。 目前請只使用第二類暫存器: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 中。

函式宣告包含:

  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

說明

在這道練習中,你會撰寫一些程式碼,幫助你依照最愛的食譜做出美味的千層麵。

你有四個任務,全都和烹調千層麵所花費的時間有關。

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. 計算準備時間(分鐘)

定義preparation_time_in_minutes函式,它會接受你加到千層麵的層數作為參數,並回傳你準備千層麵所花的分鐘數,假設每一層需要花你 2 分鐘準備。

preparation_time_in_minutes(2);
// => 4

4. 計算經過的時間(分鐘)

定義elapsed_time_in_minutes函式,這個函式接受兩個參數,順序如下:

  1. 你加到千層麵的層數。
  2. 千層麵已經在烤箱中的分鐘數。

這個函式應該回傳你花了幾分鐘烹調千層麵。 這是準備時間(分鐘)和千層麵目前為止在烤箱中花費的時間(分鐘)的總和。

elapsed_time_in_minutes(3, 20);
// => 26
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
x86-64 Assembly Exercism

準備好開始 千層麵 了嗎?

註冊 Exercism,透過 22 個概念130 個練習 和真人引導來學習並精通 x86-64 Assembly,全部免費。