程式的記憶體通常由作業系統(OS)對應成一個通用的配置:
| 位址 | 記憶體區段 |
|---|---|
| 高位 | 堆疊 |
| ... | |
| 堆積 | |
| 可讀寫區段 | |
| 程式碼/唯讀區段 | |
| 低位 | 保留 |
以區段劃分的記憶體,會再組織成具有不同權限的節區。
我們到目前為止定義的函式,全都位於 section .text 中。 這個節區存放的是唯讀、可執行的資料。 其他節區則用來宣告資料變數,這些變數可能是唯讀或可讀寫,但不可執行。
初始化過的資料會宣告在section .data中。
在 NASM(The Netwide Assembler,也就是本學習路徑使用的組譯器)中,初始化過的變數有名稱、一個指出資料大小的指示詞,以及一串以逗號分隔的值。
這些部分彼此之間以空格分隔,而標籤後面可以選擇性加上 :。
主要的指示詞與其對應的資料大小如下:
| 指示詞 | 大小 |
|---|---|
| db | 1 位元組 |
| dw | 2 位元組 |
| dd | 4 位元組 |
| dq | 8 位元組 |
例如,以下宣告了一個名為 space、值為 10 的位元組變數:
section .data
space db 10
宣告在 section .data 中的變數是可變的,也就是可讀寫。
它們也具有_靜態_儲存期,代表它們在整個程式執行期間都存在。
section .rodata與 section .data 類似。
兩個節區都含有初始化過的資料,宣告方式相同,儲存期也相同。
兩者主要的差別在於,section .rodata 中的資料是不可變的,也就是唯讀。
用 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]
雖然可以用 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é 是當地一所學校的老師。 他想到了一些有趣的實驗,用來展示如何組合顏色以產生不同的顏色。
他請你協助進行這些實驗。
在本練習中,顏色以一個 32 位元(4 位元組)的數字來表示,並編碼其RGB值。
RGB值由 3 個通道組成:紅、綠_和_藍,每個通道佔 8 位元(1 位元組)。
第四個位元組通常保留給 _Alpha_通道,但在本練習中它的值會是空的(0)。
每種顏色的值都已儲存在一張表格中,該表格定義在另一個原始檔裡。 顏色在這張表格中以一個唯一的位址來識別。
定義一個函式get_color_value,它會回傳某個顏色的 32 位元值。
這個函式接受此顏色在顏色表中的有效位址作為參數。
get_color_value(black)
// => 0
提示:32 位元等於 4 位元組。
為了混合不同的顏色,José 會先固定一個基準色,只改變與它組合的次要色。
定義一個函式add_base_color,將某個顏色的 32 位元值儲存到變數base_color中,以便之後使用。
這個函式沒有回傳值,並接受該顏色在顏色表中的位址作為參數。
變數base_color由你定義,且必須能從其他原始檔存取。
同一時間最多只會有一個基準色。 如果加入了新的基準色,舊的就會被捨棄。
預設情況下,在程式開始時,base_color應該以 _白色_的 32 位元值初始化,也就是0xFFFFFF00。
提示:NASM 接受以0x開頭定義的十六進位數字,例如0xFFFFFF00。
José 預期會用原色做出許多組合,所以希望把它們分開,以便快速存取。
由於他用RGB來表示顏色,這些原色是:
RED,值為0xFF000000。GREEN,值為0x00FF0000。BLUE,值為0x0000FF00。為這些顏色各定義一個常數。 這些常數必須能從其他原始檔存取。
顏色的組合應依照另一個原始檔中定義的combining_function來進行。
這個函式接受base_color的 32 位元值,以及要與它混合的次要色的 32 位元值作為參數。
它會回傳組合後顏色的 32 位元值。
定義一個函式make_color_combination,它會組合兩個顏色並將結果儲存到記憶體中。
這個函式沒有回傳值,並依下列順序接受參數:
請注意,combining_function可能會修改你正在使用的暫存器中的值。
在呼叫這個函式之前,請務必將你需要的任何變數儲存到記憶體中。