有個特別的暫存器叫做rflags。
它的各個位元會像旗標一樣,對應各種不同的條件。
底下列出其中幾個:
| 名稱 | 符號 | 位元 |
|---|---|---|
| carry | CF | 0 |
| zero | ZF | 6 |
| sign | SF | 7 |
| overflow | OF | 11 |
rflags裡的旗標並_不會_被直接修改。
它們是由許多不同的指令所設定的。
舉例來說,當運算結果為零時,許多算術或位元運算都會設定ZF。
測試條件時最常用的指令之一就是cmp。
它接受兩個運算元並更新旗標,但不會修改它的運算元。
cmp指令會從第一個運算元減去第二個運算元,並根據結果設定旗標。
若 A 是第一運算元,B 是第二運算元:
| 旗標 | 設定時機 |
|---|---|
| CF | A < B(無號) |
| ZF | A == B |
| SF | A < B(有號,無溢位) |
| OF | 有號減法中的溢位 |
預設情況下,x86-64 的程式碼會由上到下依序執行。
但有很多情況必須改變這種行為。 舉例來說,根據某個條件執行一組不同的指令。
在高階語言中,這通常是透過if...else這類條件判斷等抽象來完成的。
不過,x86-64 裡並沒有這些東西。
取而代之的是,x86-64 提供了能把執行流程轉移到程式碼另一個位置的指令。
這就叫做branching。
我們已經看過兩個這樣的指令:call和ret。
當一個函式被呼叫時,執行流程會從呼叫端轉移到被呼叫的函式。 而回傳時,執行流程會轉移回呼叫端。
如果找不到ret,執行流程會從一個函式直接落到下一個函式。
這有時可以用來最佳化程式碼流程。
jmp指令會無條件地把程式的執行流程轉移到程式碼的另一個位置。 它唯一的運算元是一個標籤,這個標籤帶有執行流程將繼續的位址。
舉例來說,看看底下的函式:
fn:
mov rax, 5
jmp end
add rax, 10
end:
ret
呼叫fn時,執行會從mov rax, 5開始。
這會把rax的值設成 5。
下一個指令是jmp end,它會把執行流程轉移到標籤end。
end之後的下一個指令是ret,它會把執行流程轉移回呼叫端函式。
注意,由於add rax, 10位於jmp end之後、end之前,所以它永遠不會被執行。
fn回傳時,rax的值是 5。
jcc這類指令只有在某個特定條件成立時,才會把程式的執行流程轉移到另一個位置。 否則,執行會依序繼續下去。
每個條件對應到rflags裡的一個或多個旗標。
有些jcc變體測試旗標是否被設定,有些則測試旗標是否被清除。
jcc裡的cc並非字面上的意思,而是指與所測試旗標相關的特定後綴。
後綴有很多種,其中許多測試的條件和其他後綴相同。 有些後綴直接對應到某個旗標,因此當該特定旗標被設定時,指令就會跳到某個標籤:
| 後綴 | 跳躍條件 |
|---|---|
| z | ZF == 1 |
| c | CF == 1 |
| s | SF == 1 |
| o | OF == 1 |
還有許多其他後綴,是為了對應它們在cmp指令中的意義而選用的。
例如:
| 指令 | 後綴 | 跳躍條件 |
|---|---|---|
| cmp A, B | e | A == B |
| cmp A, B | l | A < B(有號) |
| cmp A, B | b | A < B(無號) |
| cmp A, B | g | A > B(有號) |
| cmp A, B | a | A > B(無號) |
可以在l、b、g或a後面加上e,把相等也納入條件中:
cmp rcx, r8
jge two ; this jumps to 'two' if rcx is greater than, or equal to, r8 in a signed comparison
jbe two ; this jumps to 'two' if rcx is lesser than, or equal to, r8 in an unsigned comparison
所有後綴都有檢查相反情況的變體。
它們的語法相同,只是在後綴前面多了一個n。
舉例來說,當ZF沒有被設定時,jnz就會跳躍。
同樣地,當 A 不 >= B 時,jnae就會跳躍(A 和 B 以無號整數解讀)。
有些後綴是相同條件的別名。
舉例來說,jz(後綴z,代表ZF)和je(後綴e,代表相等)在ZF被設定時都會跳躍。
這是因為在使用cmp時,當減法結果為零,ZF就會被設定,而這對應到兩個運算元相等。
不過,其他後綴測試的是旗標的組合,無法直接用單一旗標的後綴取代。
請優先選用最能描述你比較語意的後綴。
標籤在整份原始碼檔案中都可見,它們不是函式內的區域標籤。 因此無法重複使用同一個標籤名稱。
為了模擬區域標籤的行為,NASM 對以句點(.)開頭宣告的標籤有種特別的寫法。
這種寫法定義的標籤會隱含前一個非句點標籤的名稱:
section .text
fn1:
...
.example: ; this is fn1.example
...
ret
fn2:
...
.example: ; this is fn2.example
...
ret
還是可以在程式碼的任何地方,用完整的標籤名稱跳到這個標籤,例如jmp fn1.example。
不過,若跳躍時只用標籤從句點開始的那部分,則會跳到上層函式_內部_的那個標籤。
例如,.example的行為就像它是該函式的區域標籤一樣:
section .text
fn1:
...
.example:
...
jmp .example ; this jumps to fn1.example
fn2:
...
.example:
...
jmp .example ; this jumps to fn2.example
在這個練習中,你將實作 Blackjack 的一些規則,例如遊戲進行的方式和計分方式。
在這個練習中,牌是以數字表示;每張數字牌以其數值表示,而 J、Q、K 和 A 則分別以 11、12、13、14 表示(遊戲中不使用鬼牌)。
為了讓這種表示方式更容易使用,檔案開頭定義了一些常數;C2 到 C10 代表數字牌,CJ 代表 J,CQ 代表 Q,CK 代表 K,CA 代表 A。
這裡假設使用的是一副標準法式 52 張牌組,但在大多數玩法中,會把好幾副牌洗在一起使用。
以下是這個概念中提到的指令:
| 指令 | 說明 |
|---|---|
| cmp a, b | 依 a - b 設定旗標 |
| jmp a | 程式碼在此停止執行,並從標籤 a 繼續 |
| jcc a | 若符合 cc 中的條件,程式碼會從標籤 a 繼續 |
以下是 cmp a, b 之後,jcc 會檢查的條件:
| 指令 | 跳躍時機 |
|---|---|
| je | a == b |
| jl | a < b(有號) |
| jg | a > b(有號) |
| jb | a < b(無號) |
| ja | a > b(無號) |
| jle | a <= b(有號) |
| jge | a >= b(無號) |
| jbe | a <= b(無號) |
| jae | a >= b(無號) |
| jne | a != b |
| jnl | !(a < b)(有號) |
| jng | !(a > b)(有號) |
| jnb | !(a < b)(無號) |
| jna | !(a > b)(無號) |
| jnle | !(a <= b)(有號) |
| jnge | !(a >= b)(有號) |
| jnbe | !(a <= b)(無號) |
| jnae | !(a >= b)(無號) |
在Blackjack中,CA的值要依手牌而定,可能是 1 或 11(這點稍後會再說明)。
人頭牌(CJ、CQ 和 CK)計為 10 點,其他任何牌則按其_數值_計分。
定義 value_of_card 函式,參數為 card,也就是代表一張牌的數字。
這個函式必須回傳傳入牌張的_數值_。
由於 CA 可以有多種值(1 或 11),我們暫時把 CA 的值固定為 1。
之後你會實作一個函式,在給定現有手牌的情況下判斷 CA 的值。
value_of_card(13)
// => 10
value_of_card(4)
// => 4
value_of_card(14)
// => 1
定義 higher_card 函式,參數為 card_one 和 card_two,這兩個數字各代表一張牌。
就計分而言,CJ、CQ 和 CK 的值都是 10。
這個函式必須回傳哪張牌的計分值較高。
如果兩張牌的值相等,就兩張都回傳。
CA 可以有多種值,所以這個任務中我們把它的值固定為 1。
higher_card(13, 11)
// => {13, 11}
higher_card(4, 6)
// => 6
>>> higher_card(13, 14)
// => 13
若要從函式回傳兩個整數,你應該同時使用 rax 和 rdx 這兩個暫存器:
returning_two_values:
mov rax, rdi
mov rdx, rsi
ret
如果只回傳一張牌,rdx 必須設為 0。
如前所述,CA 的值_可以_是 1 或 11 點,取決於手牌。
Blackjack 的規則要求 CA 的取值要讓手牌分數最大化,但不能超過 21(也就是「爆牌」)。
定義 value_of_ace 函式,參數為 card_one 和 card_two,這兩個數字代表在拿到最後一張 CA_之前_手牌中已有的兩張牌。
你的函式必須回傳即將加入的 CA 會被指定為 1 還是 11。
記住:加入 CA 後的新手牌分數要盡可能高,但_不能_超過 21。
提示:如果手牌裡已經有一張 CA,那麼接下來這張 CA 的值就會是 1。
value_of_ace(6, 13)
// => 1
value_of_ace(7, 3)
// => 11
如果玩家拿到的前兩張牌是 CA 和一張十點牌(C10、CJ、CQ 或 CK),那麼這位玩家的分數就是 21。
這樣的手牌稱為 Blackjack。
定義 is_blackjack 函式,參數為 card_one 和 card_two,這兩個數字代表一對牌。
如果這兩張牌是 Blackjack,函式必須回傳 1,否則回傳 0。
為了讓這些值更容易使用,檔案開頭定義了常數 TRUE 和 FALSE,分別等同於 1 和 0。
注意:分數的_計算_方式有很多種。
但如果可以,我們希望你檢查手牌中**有沒有** CA 和一張十點牌,而不是把牌的數值_加總_起來。
is_blackjack(14, 13)
// => 1
is_blackjack(10, 9)
// => 0
如果玩家的前兩張牌值相同(例如手牌是兩張 C6,或是 CQ 加 CK),玩家可以選擇把它們當成兩組獨立的手牌。
這就叫做「分牌」。
定義 can_split_pairs 函式,參數為 card_one 和 card_two,這兩個數字代表一對牌。
如果這兩張牌可以分成兩組手牌,函式必須回傳 1,否則回傳 0。
為了讓這些值更容易使用,檔案開頭定義了常數 TRUE 和 FALSE,分別等同於 1 和 0。
can_split_pair(12, 13)
// => 1
can_split_pair(10, 14)
// => 0
當最初發的兩張牌合計為 9、10 或 11 點時,玩家可以再下一個與原注相同金額的賭注。 這就叫做「加倍下注」。
定義 can_double_down 函式,參數為 card_one 和 card_two,這兩個數字代表一對牌。
如果這兩張牌可以讓玩家「加倍下注」,函式必須回傳 1,否則回傳 0。
為了讓這些值更容易使用,檔案開頭定義了常數 TRUE 和 FALSE,分別等同於 1 和 0。
can_double_down(14, 9)
// => 1
can_double_down(10, 2)
// => 0