调色板

调色板

学习练习

简介

内存

操作系统(OS)通常会为程序按一个大致的布局映射内存:

地址 内存区域
高地址 栈
...
堆
可读写段
代码/只读段
低地址 保留区

内存按段划分,段内又划分为节,各节拥有不同的权限。

到目前为止,我们定义的函数全都位于**.text 节**中。 这个节存放只读的可执行数据。 其他节用于声明数据变量,这些变量可以是只读的,也可以是可读写的,但它们不可执行。

.data 节

已初始化的数据在**.data 节**中声明。

在 NASM(The Netwide Assembler,本 track 使用的汇编器)中,一个已初始化的变量包含名称、指明数据大小的伪指令,以及一组用逗号分隔的值。 这三部分之间用空格分隔,标签后面还可以(可选地)跟一个 :。

主要的伪指令及其对应的数据大小如下:

伪指令 大小
db 1 字节
dw 2 字节
dd 4 字节
dq 8 字节

例如,下面声明了一个名为 space 的字节变量,其值为 10:

section .data
    space db 10

在 section .data 中声明的变量是可变的,也就是说,它们可读可写。 它们还具有_静态_存储期,这意味着它们在程序的整个运行期间都存在。

.rodata 节

.rodata 节与 section .data 类似。 两个节都包含已初始化的数据,声明方式相同,存储期也相同。

它们的主要区别在于,section .rodata 中的数据是不可变的,也就是只读的。

Note

用 equ 定义的常量与在 section .rodata 中定义的常量不同。

用 equ 定义的常量不占用内存空间,汇编器会直接用它的值进行替换。 它实际上只是该值的一个占位符。

另一方面,在 section .rodata 中定义的常量则真正存储在内存中,拥有自己的地址。

访问数据

标签与间接寻址

声明的数据必须有一个与之关联的名称。 这个名称称为标签。

标签是一个符号,它记录了数据在内存中的具体地址。 x86-64 中的地址是 64 位的值。

在 NASM 中,直接用标签访问数据得到的不是所分配的内存,而是它的地址:

section .data
    example dq 27 ; this declares a 8-byte variable initialized with 27

section .text
fn:
    mov rax, example ; this stores the address of the declared variable in rax, not its contents
    ...

要访问某个内存地址中的内容,必须对它进行_解引用_。 这称为间接寻址。

在 NASM 中,这通过[]完成:

section .data
    example dq -27 ; this declares a 8-byte variable initialized with -27

section .text
fn:
    mov rax, [example] ; this dereferences example and access the value stored in memory (-27)
    ...

不过,有些情况下,被解引用的内存大小可能存在歧义。 这时必须使用一个指定该大小的前缀。

下面是典型的 x86-64 程序中最常用的前缀及其大小:

前缀 大小
byte 1 字节
word 2 字节
dword 4 字节
qword 8 字节

同一次加载也可以明确写出大小:

    mov rax, qword [example] ; same dereference, size stated explicitly

解引用内存时,始终加上前缀是个好习惯。

写入内存

写入内存的方式相同,也是通过解引用一个地址:

section .data
    example1 db 10            ; example1 is a 1-byte memory location initialized with value 10
    example2 dq -456          ; example2 is a 8-byte memory location initialized with value -456
    example3 dd 54            ; example3 is a 4-byte memory location initialized with value 54

section .text
fn:
    mov byte [example1], 20   ; example1 now has value 20
    mov qword [example2], rdx ; example2 now has value equal to the contents in rdx
    mov dword [example3], eax ; example3 now has value equal to the contents in eax

注意,在大多数指令中,你可以直接使用内存操作数,而不必先把内容加载到寄存器里。 不过,通常不能把内存操作数同时用于源操作数和目标操作数,只能用在其中一边:

section .data
    example4 dw 4
    example5 dq -8
    example6 dd 15

section .text
fn:
    add word [example4], 5     ; example4 is now a 2-byte memory location with the value 4 + 5 = 9
    imul rax, qword [example5] ; rax = rax * (-8)
    ; this is not possible -> sub dword [example6], dword [example6]
LEA 指令

虽然可以用 mov 把变量的地址存进寄存器,但还有一条专门用于此目的的指令:lea。

这条指令使用内存形式的操作数,但它_不_读取内存。 相反,它计算有效地址表达式,并把结果写入目标操作数:

lea rax, [example] ; this stores the address of 'example' in rax

用 lea 计算内存地址并将其存入寄存器,是更地道的写法。

相对寻址

访问内存位置时,NASM 的默认行为是生成绝对地址,也就是固定的内存地址。

出于安全考虑,可执行文件通常构建为PIE(位置无关可执行文件),其中的内存区域会被放在随机化的位置。 在PIE中,变量的最终地址在链接时并不确定。 所以,代码改为把地址计算为相对于一个特殊寄存器 rip 中值的偏移量,这个寄存器指向下一条将要执行的指令。

这通常称为RIP 相对寻址。

在 NASM 中,可以用rel运算符请求 RIP 相对寻址:

mov rax, qword [rel variable]

也可以在源文件顶部用default rel把相对寻址设为默认方式。

本 track 中的所有练习都以PIE形式编译和链接,因此应使用rel来生成相对地址。

可见性

在任何节(例如.text、.data、.rodata)中定义的标签(函数和数据)在该源文件内可见。 如果声明为 global,它们对其他源文件也可见。

反过来,其他源文件中定义的标签如果声明为 extern,则对当前源文件可见。 这种情况下,汇编代码中不会标明数据大小,必须事先知道它。

default rel

section .data

global number1 ; 'number1' is a variable visible to other source files
number1 db 200

extern number2 ; 'number2' is a variable visible to the current source file, but defined in another

section .text

extern sum ; sum is a function visible to the current source file, but defined in another

fn:
    mov dil, byte [number1]
    mov sil, byte [number2]
    call sum
    ...

说明

你的朋友 José 是当地一所学校的老师。 他想到了一些有趣的实验,用来展示不同的颜色如何组合产生新的颜色。

他请你帮忙完成这些实验。

Note

在本练习中,颜色用一个 32 位(4 字节)的数字来表示,其中编码了它的RGB值。

一个RGB值由 3 个通道组成,即_红_、绿_和_蓝,每个通道占 8 位(1 字节)。 第四个字节通常保留给 Alpha 通道,但在本练习中它的值会是空的(0)。

1. 获取颜色的 RGB 值

每种颜色的值已经存放在一张表中,这张表定义在另一个源文件里。 颜色通过它在表中的唯一地址来标识。

定义一个函数get_color_value,它返回某个颜色的 32 位值。 这个函数接受该颜色在颜色表中的有效地址作为形参。

get_color_value(black)
// => 0

提示 - 32 位等于 4 字节。

2. 添加基础颜色

为了混合不同的颜色,José 会先固定一个基础颜色,然后只改变与它组合的次要颜色。

定义一个函数add_base_color,它把某个颜色的 32 位值保存到变量base_color中,以便之后使用。 这个函数没有返回值,它接受该颜色在颜色表中的地址作为形参。

变量base_color由你定义,并且必须能从其他源文件访问。

同一时间最多只有一个基础颜色。 如果添加了新的基础颜色,旧的那个就会被丢弃。

默认情况下,在程序开始时,base_color应该被初始化为 白色 的 32 位值,即0xFFFFFF00。

提示 - NASM 接受用0x开头定义的十六进制数字,例如0xFFFFFF00。

3. 定义原色的常量

José 打算用原色做很多组合,所以他希望把它们单独列出来,以便快速访问。 因为他用RGB来表示颜色,所以原色有:

  • RED,值为0xFF000000。
  • GREEN,值为0x00FF0000。
  • BLUE,值为0x0000FF00。

为每种颜色各定义一个常量。 这些常量必须能从其他源文件访问。

4. 组合颜色

颜色应当按照另一个源文件中定义的combining_function来组合。 这个函数接受base_color和要与之混合的次要颜色的 32 位值作为形参。 它返回组合后颜色的 32 位值。

定义一个函数make_color_combination,它组合两种颜色,并把结果保存到内存中。 这个函数没有返回值,它按以下顺序接受这些形参:

  • 组合后颜色的 32 位值应当存储的地址。
  • 颜色表中某个次要颜色的地址,这个颜色要与基础颜色组合。
Caution

注意,combining_function可能会修改你正在使用的寄存器中的值。 在调用这个函数之前,一定要把需要用到的变量保存到内存中。

通过 GitHub 编辑 链接将在新窗口或新标签页中打开
x86-64 Assembly Exercism

准备好开始 调色板 了吗?

注册 Exercism,借助 22 个概念130 个练习 和真人导师指导,学习并掌握 x86-64 Assembly,全部免费。