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。
通常使用push指令把寄存器保存到栈上。
然后在函数结束前使用pop指令把它们恢复。
指令是 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中。
一个函数声明由以下部分组成:
:。实参如何传入函数、如何从函数返回,都有特定的约定。 两种主要的调用约定是 System V AMD64 ABI 和 Microsoft x64。
本轨道使用 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