調色盤

調色盤

學習練習

簡介

記憶體

程式的記憶體通常由作業系統(OS)對應成一個通用的配置:

位址 記憶體區段
高位 堆疊
...
堆積
可讀寫區段
程式碼/唯讀區段
低位 保留

以區段劃分的記憶體,會再組織成具有不同權限的節區。

我們到目前為止定義的函式,全都位於 section .text 中。 這個節區存放的是唯讀、可執行的資料。 其他節區則用來宣告資料變數,這些變數可能是唯讀或可讀寫,但不可執行。

.data 節區

初始化過的資料會宣告在section .data中。

在 NASM(The Netwide Assembler,也就是本學習路徑使用的組譯器)中,初始化過的變數有名稱、一個指出資料大小的指示詞,以及一串以逗號分隔的值。 這些部分彼此之間以空格分隔,而標籤後面可以選擇性加上 :。

主要的指示詞與其對應的資料大小如下:

指示詞 大小
db 1 位元組
dw 2 位元組
dd 4 位元組
dq 8 位元組

例如,以下宣告了一個名為 space、值為 10 的位元組變數:

section .data
    space db 10

宣告在 section .data 中的變數是可變的,也就是可讀寫。 它們也具有_靜態_儲存期,代表它們在整個程式執行期間都存在。

.rodata 節區

section .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(Position Independent Executable,位置無關可執行檔),其中記憶體區段會被放在隨機化的位置。 在 PIE 中,變數的最終位址在連結時並未知曉。 因此,程式碼改為把位址計算成某個特殊暫存器 rip 中值的偏移量,而 rip 會指向下一條要執行的指令。

這通常稱為 RIP 相對定址。

在 NASM 中,你可以用 rel 運算子要求使用 RIP 相對存取:

mov rax, qword [rel variable]

也可以在原始碼檔案最上方加上 default rel,把相對定址設為預設。

本學習路徑中的所有練習都會以 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 位元值作為參數。 它會回傳組合後顏色的 32 位元值。

定義一個函式make_color_combination,它會組合兩個顏色並將結果儲存到記憶體中。 這個函式沒有回傳值,並依下列順序接受參數:

  • 組合後顏色的 32 位元值要儲存的位址。
  • 次要色在顏色表中的位址,用來與主色組合。
Caution

請注意,combining_function可能會修改你正在使用的暫存器中的值。 在呼叫這個函式之前,請務必將你需要的任何變數儲存到記憶體中。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
x86-64 Assembly Exercism

準備好開始 調色盤 了嗎?

註冊 Exercism,透過 22 個概念130 個練習 和真人引導來學習並精通 x86-64 Assembly,全部免費。