千层面

千层面

学习练习

简介

基础

注释

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 位中,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中。

一个函数声明由以下部分组成:

  1. 一个带有函数名的标签,后面跟一个:。
  2. 定义该函数的指令。
  3. 返回指令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

说明

在这个练习中,你将编写一些代码,帮你照着最喜欢的烹饪书做出一份美味的千层面。

你有四个任务,都和烹饪千层面所花的时间有关。

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,全部免费。