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 位中,rax是该指令的目标操作数。
在大多数指令中,可以使用常量整数作为源操作数(但不能作为目标操作数):
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 调用约定,前六个整数参数通过寄存器传给函数。
它们的传递顺序如下: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