有一个特殊的寄存器叫rflags。
它的各个位就像各种条件的标志位。
下面列出了其中一些:
| 名称 | 符号 | 位 |
|---|---|---|
| 进位 | CF | 0 |
| 零 | ZF | 6 |
| 符号 | SF | 7 |
| 溢出 | 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
在本练习中,你将实现二十一点的一些规则,比如游戏的玩法和计分方式。
在本练习中,牌用数字表示;每张数字牌用它的数值表示,而 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)(无符号) |
在二十一点中,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,取决于手牌。
二十一点的规则要求选择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。
这种手牌称为二十一点。
定义is_blackjack函数,形参为card_one和card_two,它们是两个数字,表示两张牌。
如果这两张牌构成二十一点,函数必须返回 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