# 電腦系統_第三章
###### tags: `Computer System`
## 前言
GCC 中 C 語言的編譯器會將 C 語言程式碼以組合語言的形式輸出,而組合語言經過組譯器之後產生機器程式碼 (mahcine code),在這一個篇章中,我們將會探討組合語言與機器程式碼。
我們使用高階語言進行程式碼撰寫,如使用 C 語言或是 JAVA 語言時,這一些程式碼提供了我們一層抽象,這一層抽象能夠讓我們在不用知道電腦底層細節也可以撰寫程式碼。而編譯器提供的型別檢查可以讓我們發現許多程式的錯誤,保證能夠按照一定的方式來引用和對數據的處理。使用高階語言撰寫的程式碼可以在許多機器上面執行,而組合語言則不一定,組合語言會依賴於機器的環境,如不同的指令集架構,如 x86 或 RISC-V。
指令集架構 (Architecture, instruction set architecture, ISA): 為處理器設計的一部份,指令集架構包含一系列的操作碼 (opcode) 以及由特定處理器所執行的指令,指令集架構包含了指令集,資料型別,定址模式,中斷異常等等。
微架構 (Microarchitecture): 為指令集架構的實現,微架構使得指令集架構可以在處理器上面被執行,微架構決定處理器內部以及其他組件如何去執行指令集架構。微架構通常以流程圖進行描述,表示機器內部元件的溝通狀況,如從一個閘 (gate) 或是暫存器,再到 ALU 的資料路徑或是控制路。
不同的微架構電腦可以共享一種指令集,如 Intel Pentium 和 AMD Athlon 這兩種微架構都實現了 x86 指令集架構,但是內部設計上有所不同。
## 關於程式的編譯
假設有一個 C 語言程式,有 `p1.c` 和 `p2.c`,我們在 Unix 底下編譯這一些程式碼
```shell
gcc -Og -o p p1.c p2.c
```
- gcc 意義為 GCC C 編譯器,這是 Linux 系統上預設的編譯器
- -Og 為 GCC C 編譯器的編譯選項,目的是為了讓我們更好的進行除錯 (debugging),讓產生出的組合語言更加方便閱讀與除錯,對於要產生出用來除錯的程式碼,`-Og` 是比 `-O0` 更好的選擇,`-Og` 會啟用部分不會影響到除錯的優化選項
- -o 為將產生出來的執行檔以給定的字串進行命名
GCC 本質上一整套程式,裡面包含編譯器,組譯器等等。GCC 在處理 C 語言程式碼時,首先 C 預處理器會將 #include 指定的檔案以及 #define 的巨集進行展開,接著,編譯器產生兩個二進位的 Object code 檔案,分別為 `p1.o` 和 `p2.o`,Object code 為一種機器語言,裡面會將所有指令以二進位進行表示,但是還沒有填入全域變數的記憶體地址。最後,鏈接器會將兩個 Object code 檔案與函式庫的函式,如 printf 進行並,並最終產生出一個名稱為 p 的可執行檔案。

### 關於機器語言
在電腦中許多地方都使用到了抽象的封裝概念,如前面看到的高階語言對於低階語言,如機器語言的封裝,而對於面向機器語言的程式碼撰寫,有兩層十分重要的抽象思想。
第一個為指令集架構 (Instruction Set Architecture, ISA) 用來定義機器語言程式的格式和行為,諸如 x86-64,把機器語言程式的行為描述成指令似乎都是按照順序執行的,但實際上處理器對於指令的處理是遠遠更加複雜的,且還涉及並行處理等等議題。但是通過 ISA,可以保證整理機器的行為和 ISA 指定的指令執行順序有一樣的行為
第二個為在機器語言程式編寫中,使用的記憶體地址為虛擬記憶體地址,並非實體物理記憶體地址,這一點通過多個硬體暫存器和作業系統等等結合起來所達成。
編譯過程中,C 語言會被轉換成處理器能夠執行的機器指令,中間經過了巨集展開的程式碼,組合語言,組合語言相比於機器語言,提供了更加方便閱讀的格式,如將某一些指令以助憶符進行表示,方便閱讀,並且讓我們理解組合語言和 C 語言之間的關係以及連繫。
從撰寫機器語言的角度,在撰寫組合語言的時候,和 C 語言有許多差別,以機器語言的觀點,機器語言相對於 C 語言,可以看見許多關於 CPU 的狀態,以下舉例
- 程式計數器 (Program Counter, PC): 在 x86-64 中用 %rip 表示,為一個暫存器,裡面儲存了下一條指令的記憶體地址
- 暫存器: 紀錄程式目前狀態,或是程式執行時產生的臨時變數,例如區域變數或是函式的回傳值
- 條件值暫存器 (condition code register): 儲存算術或是邏輯運算的資訊,如正負值,是否為零,這一些值會用來實現 if 和 while 或是其他條件分支
- 記憶體部分: 在機器語言的觀點,記憶體不同於在 C 語言中有許多資料型別等等,機器語言的觀點是將所有資料以連續的 bytes 進行展示。
一個程式所使用的記憶體,包含程式可執行的機器語言程式碼,作業系統需要的一些資訊,用來管理函式呼叫和回傳的 stack 部分,以及用來動態分配記憶體,如 malloc 等等的 heap 部分。
程式記憶體是使用虛擬記憶體進行尋址的,在程式執行時,只有某一個部分的虛擬記憶體地址是合法能夠存取的。例如在 x86-64 的虛擬記憶體地址是由 64 bytes 表示。高位 16 位會設定為 0,所以實際上的定址範圍為 $0 \sim 2^{48}$ bytes。程式在存取虛擬記憶體地址值,作業系統會負責將虛擬記憶體地址轉換到實體物理記憶體上。
機器指令通常只執行一個基本的操作,例如將某個值移動到暫存器中,或是將暫存器中的數值相加。編譯器需要將高階程式碼以合適的方式產生出這一些機器指令,從而運用機器指令實現高階程式碼中的賦值,循環等等操作。
## C 語言,組合語言,目的碼,執行檔
看到以下 C 語言程式碼
```c
#include <stdio.h>
#include <stdlib.h>
long plus(long x, long y) {
return x + y;
}
void sumstore(long x, long y, long *dest) {
long t = plus(x, y);
*dest = t;
}
int main(int argc, char *argv[]) {
long x = atoi(argv[1]);
long y = atoi(argv[2]);
long z;
sumstore(x, y, &z);
printf("%ld + %ld --> %ld\n", x, y, z);
return 0;
}
```
接著使用以下指令對以上程式碼進行編譯,使用 `-S` 選項產生出組合語言程式碼
```shell
gcc -og -S test.c
```
經過上面的指令編譯過後,可以發現到只產生了一個組合語言的檔案,而沒有產生執行檔,實際上,gcc 是編譯工具的工具集,裡面包含了組譯器,鏈接器等等,上面這一段指令使用了 gcc 中的編譯器編譯產生了一個組合語言的檔案 (組合語言檔案內容取決於機器環境本身,可能內容會有所差別,可以使用 [godblot](https://godbolt.org/) 進行測試,比較不同編譯環境的差別)
以下為部分組合語言程式碼內容 (使用 godblot x86-64 gcc 12.2)
```asm
plus:
lea rax, [rdi+rsi]
ret
sumstore:
push rbx
mov rbx, rdx
call plus
mov QWORD PTR [rbx], rax
pop rbx
ret
```
上面每一個內縮一個縮排的程式碼表示一條對應的機器語言指令,例如 `push rbx` 表示將 `rbx` 暫存器的內容押入到程式的記憶體 stack 區域中
以下為部分組合語言程式碼內容 (未經過省略,完整的組合語言程式碼內容)
```asm
plus:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp)
movq %rsi, -16(%rbp)
movq -8(%rbp), %rdx
movq -16(%rbp), %rax
addq %rdx, %rax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size plus, .-plus
.globl sumstore
.type sumstore, @function
```
開頭 . 的部分,如 `.cfi_startproc` 並非是 C 語言程式碼的一部分,而是用來給除錯器 (debugger) 所使用,目的是提供一些資訊給除錯器,用來定位程式碼。
接著我們使用以下指令對 `test.c` 進行編譯並組譯,產生出其目的碼 (object code)
```shell
gcc test.c -c -Og -o test
```
編譯完成後,我們會得到一個二進位檔案,我們使用二進位檢視器進行檢視 (使用 HXD 進行檢視)

這一些十六進位的目的碼都對應到了上面一條組合語言的指令,我們可以使用 godblot 進行對照

在 Linux 上,如果要檢視目的碼檔案的內容,我們可以嘗試使用反組譯器 (disassembler),反組譯器會根據目的碼的內容,產生出類似於組合語言的格式提供給我們閱讀,使用 objdump 對目的碼進行反組譯,產生出組合語言
```obj
0000000000000000 <plus>:
0: f3 0f 1e fa endbr64
4: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
8: c3 retq
0000000000000009 <sumstore>:
9: f3 0f 1e fa endbr64
d: 53 push %rbx
e: 48 89 d3 mov %rdx,%rbx
11: e8 00 00 00 00 callq 16 <sumstore+0xd>
16: 48 89 03 mov %rax,(%rbx)
19: 5b pop %rbx
1a: c3 retq
000000000000001b <main>:
1b: f3 0f 1e fa endbr64
1f: 41 54 push %r12
21: 55 push %rbp
22: 53 push %rbx
23: 48 83 ec 10 sub $0x10,%rsp
27: 48 89 f5 mov %rsi,%rbp
2a: 41 bc 28 00 00 00 mov $0x28,%r12d
30: 64 49 8b 04 24 mov %fs:(%r12),%rax
35: 48 89 44 24 08 mov %rax,0x8(%rsp)
3a: 31 c0 xor %eax,%eax
3c: 48 8b 7e 08 mov 0x8(%rsi),%rdi
40: ba 0a 00 00 00 mov $0xa,%edx
45: be 00 00 00 00 mov $0x0,%esi
4a: e8 00 00 00 00 callq 4f <main+0x34>
4f: 48 63 d8 movslq %eax,%rbx
52: 48 8b 7d 10 mov 0x10(%rbp),%rdi
56: ba 0a 00 00 00 mov $0xa,%edx
5b: be 00 00 00 00 mov $0x0,%esi
60: e8 00 00 00 00 callq 65 <main+0x4a>
65: 48 63 e8 movslq %eax,%rbp
68: 48 89 e2 mov %rsp,%rdx
6b: 48 89 ee mov %rbp,%rsi
6e: 48 89 df mov %rbx,%rdi
71: e8 00 00 00 00 callq 76 <main+0x5b>
76: 4c 8b 04 24 mov (%rsp),%r8
7a: 48 89 e9 mov %rbp,%rcx
7d: 48 89 da mov %rbx,%rdx
80: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 87 <main+0x6c>
87: bf 01 00 00 00 mov $0x1,%edi
8c: b8 00 00 00 00 mov $0x0,%eax
91: e8 00 00 00 00 callq 96 <main+0x7b>
96: 48 8b 44 24 08 mov 0x8(%rsp),%rax
9b: 64 49 33 04 24 xor %fs:(%r12),%rax
a0: 75 0e jne b0 <main+0x95>
a2: b8 00 00 00 00 mov $0x0,%eax
a7: 48 83 c4 10 add $0x10,%rsp
ab: 5b pop %rbx
ac: 5d pop %rbp
ad: 41 5c pop %r12
af: c3 retq
b0: e8 00 00 00 00 callq b5 <main+0x9a>
```
我們可以看到目的碼以及目的碼轉換後對應到的組合語言形式
組合語言相較於 C 語言,有以下幾點可以注意
- 有許多不同的整數型別,大小有 1, 2, 4, 8 bytes,他們可能單純只是數值,又或者表示為記憶體地址,對於機器語言而言,他們都只是單純為一連串的數字,不同於 C 語言有整數,指標等等意義
- 浮點數可能大小為 4, 8, 10 bytes
- 對於 C 語言程式碼,背後只是一連串的組合語言指令序列
- array 或是 struct 只是一連串記憶體
對於以上從目的碼反組譯成的組合語言,有幾點可以注意
- x86-64 的指令長度從 1 到 15 bytes 不等,常用的指令以及操作數 (operator) 較少的指令所需指令長度較短,不常用或是操作數較多的指令長度較長
- 有一些指令被解析成單一個 bytes 進行表示,如 `push %rbx` 表示成 53
- 反組譯器實際上只是根據目的碼中 bytes 所構成的序列來確定要轉換成的組合語言,並不需要存取該程式的 C 語言原始碼或是組合語言檔案
- 比較由 gcc 產生的組於語言檔案以及使用 objdump 反組譯所產生的組合語言檔案,會發現 `ret`, `call` 等指令在 objdump 反組譯後後面加上了一個 `q`,在 64-bit 模式下,`ret` 表示從記憶體 stack 區域彈出一個 quadword 大小的記憶體地址,並儲存到 `%rip`,而 32-bit 則是從 stack 區域彈出一個 dword 大小的記憶體地址,並儲存到 `%eip`,而 objdump 的 `retq` 指的是在 64-bit 的情況下
接著我們可以試著利用目的碼檔案產生出一個可執行檔,這過程中我們需要鏈接器 (linker),以及在目的檔中需要存在 main 函式,產生執行檔的過程,會包含目的碼以及啟動和結束程式的程式碼,啟動程式中包含指示執行黨的進入點為 main,以及一些用來和作業系統進行交互的程式碼,以下為 `main.c`
```c
#include <stdio.h>
#include <stdlib.h>
long plus(long x, long y);
void sumstore(long x, long y, long *dest);
int main(int argc, char *argv[]) {
long x = atoi(argv[1]);
long y = atoi(argv[2]);
long z;
sumstore(x, y, &z);
printf("%ld + %ld --> %ld\n", x, y, z);
return 0;
}
```
以下為 `sum.c`
```c
long plus(long x, long y) {
return x + y;
}
void sumstore(long x, long y, long *dest) {
long t = plus(x, y);
*dest = t;
}
```
接著使用以下指令進行編譯,我們使用 gcc 對 `main.c` 和 `sum.c` 進行編譯,產生出來的執行檔命名為 `prog`
```shell
gcc -og -o prog sum.c main.c
```
整個過程,我們會產生 `sum.c` 和 `main.c` 的組合語言,接著產生出兩個目的檔,`sum.o` 和 `main.o`,經過鏈接器,將啟動和中止程式的程式碼,與作業系統互動的部分,以及 printf 等等檔案鏈接後,產生出一個可執行檔。
接著我們嘗試分別使用 objdump 對 `prog` 以及 `sum.o` 進行反組譯
```obj=
/* prog */
00000000000011a6 <sumstore>:
11a6: f3 0f 1e fa endbr64
11aa: 55 push %rbp
11ab: 48 89 e5 mov %rsp,%rbp
11ae: 48 83 ec 28 sub $0x28,%rsp
11b2: 48 89 7d e8 mov %rdi,-0x18(%rbp)
11b6: 48 89 75 e0 mov %rsi,-0x20(%rbp)
11ba: 48 89 55 d8 mov %rdx,-0x28(%rbp)
11be: 48 8b 55 e0 mov -0x20(%rbp),%rdx
11c2: 48 8b 45 e8 mov -0x18(%rbp),%rax
11c6: 48 89 d6 mov %rdx,%rsi
11c9: 48 89 c7 mov %rax,%rdi
11cc: e8 b8 ff ff ff callq 1189 <plus>
11d1: 48 89 45 f8 mov %rax,-0x8(%rbp)
11d5: 48 8b 45 d8 mov -0x28(%rbp),%rax
11d9: 48 8b 55 f8 mov -0x8(%rbp),%rdx
11dd: 48 89 10 mov %rdx,(%rax)
11e0: 90 nop
11e1: c9 leaveq
11e2: c3 retq
```
```obj=
/* sum */
000000000000001d <sumstore>:
1d: f3 0f 1e fa endbr64
21: 55 push %rbp
22: 48 89 e5 mov %rsp,%rbp
25: 48 83 ec 28 sub $0x28,%rsp
29: 48 89 7d e8 mov %rdi,-0x18(%rbp)
2d: 48 89 75 e0 mov %rsi,-0x20(%rbp)
31: 48 89 55 d8 mov %rdx,-0x28(%rbp)
35: 48 8b 55 e0 mov -0x20(%rbp),%rdx
39: 48 8b 45 e8 mov -0x18(%rbp),%rax
3d: 48 89 d6 mov %rdx,%rsi
40: 48 89 c7 mov %rax,%rdi
43: e8 00 00 00 00 callq 48 <sumstore+0x2b>
48: 48 89 45 f8 mov %rax,-0x8(%rbp)
4c: 48 8b 45 d8 mov -0x28(%rbp),%rax
50: 48 8b 55 f8 mov -0x8(%rbp),%rdx
54: 48 89 10 mov %rdx,(%rax)
57: 90 nop
58: c9 leaveq
59: c3 retq
```
可以發現到大部分程式碼皆相同,只有幾項不同之處
1. 鏈接器將 sumstore 這一段程式碼移動到了不同的記憶體地址
2. 觀察 prog 的第 14 行和 sum 的第 14 行,可以發現到 prog 填入了 plus 函式所在的記憶體地址,而 sum 的第 14 行則只是使用相對定址表示 plus 所在的記憶體地址,這裡可以看到鏈接器其中一個任務為讓函式呼叫時能夠找到該函式所在的記憶體地址
### 組合語言資料型別與大小
由於 Intel 是從 16 位元的系統架構一路發展到 32 位元的,Intel 使用 word 表示一個 16 位元的資料型別,因此 32 位元的資料型別在 Intel 中可以表示成 double words,64 位元的資料型別可以表示成 quad words。
下表為 C 語言基本資料型別對應到的 Intel x86-64 表示法

注意到浮點數有分成單精度和倍精度兩種,而在 x86 的微處理器中曾經出現過 80 位的浮點數設計用來進行浮點數的計算,這裡對應到前面說的,浮點數可能有 4,8,10 位元三種大小。可以在 C 語言中宣告 `long double` 使用這一種格式,不過這一種格式是不可移植的,且不如單精度或是倍精度來的高效,因此不建議使用。
在表格中我們可以看到組合語言後綴,這表示操作數的大小,在前面的組合語言中我們也看到了類似的用法,如 `movq`,對於資料傳輸的指令 `mov`,有 `movb` (傳送 bytes),`movw` (傳送一個 word),`movl` (傳送兩個 words) 等等。這裡可以注意到 `int` 和 `double` 都使用 `l` 作為組合語言後綴,但這並不會造成歧異,原因為浮點數是使用和整數完全不同的指令以及暫存器。
### 組合語言: 暫存器
一個 x86-64 的 CPU 包含一組 16 個用來儲存 64 位元數值的通用暫存器,這些暫存器用來儲存整數資料或是指標,他們都使用 `%r` 開頭。
最一開始 Intel 8086 有 8 個 16 位元的暫存器,也就是下表中 `%ax` 到 `%bp`,每一個暫存器都有其對應的用途。到了 IA32 架構,暫存器從 16 位元拓展到了 32 位元,從 `%eax` 到 `%ebp`,接著到了 64 位元,暫存器變成從 `%rax` 到 `%rbp`,並且另外增加了 8 個暫存器,標示 `%r8` 到 `%r15`。
在上面的表中我們可以看到許多暫存器,下面我們會看到如果在不同大小的暫存器之間傳送資料會發生什麼事情。
### 組合語言: 取值
上面看到的組合語言大多是由一個操作符號 (opcode) 加上一個或是多個操作數 (operand),操作數指示我們要使用的資料本身或是資料所在的位置,或是我們要將運算過後的結果要放置的位置。
資料可以是以常數存在,或是從某一個暫存器或是記憶體中讀取。而結果也可以放在暫存器或是記憶體中,操作數可以分成三個類型
1. 立即數 (immediate): 用來表示常數,在 ATT 格式的組合語言中為 `$` 後面加上一個 C 語言表示法的整數,如 `$-577`, `$0x1F`。對於不同的指令,對應到的 immediate 範圍也不相同
2. 暫存器 (register): 我們可以使用 `r_a` 去存取某一個暫存器 `a`,用 `R[r_a]` 表示他的值
3. 記憶體引用 (memory reference): 我們可以直接使用記憶體地址去存取該位置的資料,使用 `M_b[Addr]` 表示在記憶體地址 `Addr` 開始的地方使用 `b` 個 bytes
==TODO 直接定址與相對定址==
### 組合語言: MOV
在組合語言中,時常會使用到將資料從一個位置移動到另外一個位置的指令。常見的 `MOV` 類型的指令有四種,分別為以下
- `MOV Source, Destination`: S -> D 意義為將 S 資料放到 D (意義隨著操作數而有不同的意思)
- `movb`: 傳送 bytes
- `movw`: 傳送 word (2 bytes)
- `movl`: 傳送 double word
- `movq`: 傳送 quad word
- `movabsq Immediate Register`: 傳送 quad word 的絕對值,將 I 放到 R
`movb`, `movw` 等指令的差別為操作的資料大小不同。
在 x86-64 中有一條限制,對於 `mov` 指令,兩個操作數不能都指向記憶體地址,不能直接將 A 記憶體地址的數值直接通過 `mov` 移動到 B 記憶體中。上面這個操作需要分成兩個步驟,先將 A 暫存器地址中的數值移動到某一個暫存器中,接著將該暫存器的數值放到 B 記憶體中。
對於一個用於存放記憶體地址的暫存器,必須為 64 位元的暫存器。
以下為常見 `mov` 指令的組合
- `movl $0x4050 %eax` 立即數--暫存器 操作大小 4 bytes
- `movw %bp %sp` 暫存器--暫存器 操作大小 2 bytes
- `movb (%rdi, %rcs), %al` 記憶體--暫存器 操作大小 1 bytes
- `movb $-17, (%rsp)` 立即數--記憶體 操作大小 1 bytes
- `movq %rax, -12(%rbp)` 暫存器--記憶體 操作大小 8 bytes
以下為 `mov` 指令的範例
```=
movabsq $0x0011223344556677, %rax %rax = 0011223344556677
movb $-1, %al %rax = 00112233445566FF
movw $-1, %ax %rax = 001122334455FFFF
movl $-1, %eax %rax = 00000000FFFFFFFF
movq $-1, %rax %rax = FFFFFFFFFFFFFFFF
```
第一行: 我們將一個 quad word 傳送到 `rax` 暫存器中
第二行: 我們將 -1 放到 `al` 暫存器中,操作資料大小為 1 個 bytes,`al` 為 `rax` 低位 1 bytes 的部分,因此 `rax` 變成 `00112233445566FF`
第三行: 我們將 -1 放到 `ax` 暫存器中,操作資料大小為 1 個 word,`ax` 為 `rax` 低位 2 bytes 的部分,因此 `rax` 變成 `001122334455FFFF`
第四行: 我們將 -1 放到 `eax` 暫存器中,操作資料大小為 2 個 word,`eax` 為 4 bytes,為 `rax` 低位 4 bytes 的部分,在 x86 中,如果 `movl` 指令中目的操作數為一個暫存器時,會將目的暫存器的高位 4 bytes 設置為 0,因此會看到 `00000000FFFFFFFF` 的結果
第五行: 我們將 -1 放到 `rax` 暫存器中,操作資料大小為 4 個 word,因此我們會看到 `FFFFFFFFFFFFFFFF` 的結果
我們也可以看到,`mov` 該以怎樣的後綴進行修飾取決操作數,以下範例
```
movl %eax, (%rsp)
movw (%rax), %dx
movb $0xFF, %bl
movb (%rsp, %rdx, 4), %dl
movq (%rdx), %rax
movw %dx, (%rax)
```
Case 1. 將 `eax` 暫存器的值複製到 `rsp` 暫存器指向到的記憶體中,`eax` 中的資料為 2 word,因此使用 `movl`
Case 2. 將 `rax` 暫存器指向到的記憶體中的值複製到 `dx` 暫存器中,`dx` 暫存器接收的值大小為 1 word,因此使用 `movw`
Case 3. 將 `0xFF` 數值複製到 `bl` 暫存器中,`bl` 暫存器接收的值大小為 1 bytes,因此使用 `movb`
Case 4. 將 `rsp` 暫存器的值與 `rdx` 暫存器的值 * 4 進行相加,得到一個記憶體地址,將該記憶體地址的值複製到 `dl` 暫存器中,`dl` 暫存器接收的值為 1 bytes,因此使用 `movb`
Case 5. 將 `rdx` 暫存器指向到的記憶體地址中的值複製到 `rax` 暫存器中,`rax` 暫存器接收 4 word 大小的值,因此使用 `movq`
Case 6. 將 `dx` 暫存器的值複製到 `rax` 暫存器指向的記憶體地址中,`dx` 暫存器傳送值的大小為 1 word,因此使用 `movw`
從上面這 6 個 Case,可以看到選擇 `movl` 或是 `movb` 取決於我們要操作的資料大小。
### 組合語言: exchange 範例
以下為一段 C 語言程式碼
```c
long exchange(long *xp, long y) {
long x = *xp;
*xp = y;
return x;
}
```
以下為上面 C 語言程式碼經過 GCC 所產生的組合語言程式碼
```
exchange:
movq (%rdi), %rax
movq %rsi, (%rdi)
ret
```
從上面的組合語言程式碼,可以判斷出 xp 儲存在 `%rdi` 中,x 儲存在 `%rax` 中,xp 為指標,裡面儲存記憶體地址,由組合語言可以很直觀的看到,`(%rdi)` 意義為得到 `rdi` 暫存器中記憶體地址所指向的值。
第 2 行: 將 `rdi` 暫存器中記憶體地址所指向的值複製到 `rax` 暫存器中,對應到 C 語言的意義,就是反參考 `xp` 指標,得到 `xp` 指標所指向的值,並將該值放到變數 `x` 中
第 3 行: 將 `rsi` 暫存器的值複製到 `rdi` 暫存器記憶體地址所指向的值,對應到 C 語言的意義,就是將變數 `y` 放置到 `xp` 指標所指向的值,也就是對 `xp` 儲存的記憶體地址指向的值進行寫入。
這一段組合語言,可以看到指標與暫存器之間的關係,在 C 語言中的指標,其實就是一個存放記憶體地址的變數,而指標的指標或間接指標就是將記憶體地址中放置於暫存器中。
我們還能夠看到,區域變數 `x` 放置到暫存器中,而非記憶體地址中,原因為存取暫存器的速度快過於存取記憶體的速度。
### 組合語言: pushq, popq
我們可以將資料放到程式執行時的 stack 記憶體區域,也可以將資料從 stack 記憶體區域彈出,stack 時常用於函式呼叫處理。在 x86-64 中,stack 位於記憶體中某一個區域,由上到下逐漸增長。
`pushq` 為將資料放到 stack 中,`popq` 為將資料彈出 stack。如果我們要將一個 4 word 的值放入 stack 中,我們需要將指向 stack 頂部的指標減 8,然後將該值寫入到新的 stack 頂部的記憶體地址,對於 `pushq %rbp`,其行為等下於以下
```
subq $8, %rsp
movq %rbp, (%rsp)
```
`pushq %rbp` 對應到的機器碼只需要 1 bytes,而上面這兩條指令需要 8 bytes。
如果 `rsp` 為 `0x108`,`rax` 為 `0x123`,當我們執行 `pushq rax` 時,效果如下面所展示

在 x86-64 中,stack 會往記憶體地址低位進行增長,因此,可以看到上面我們將資料放入到 stack 後記憶體地址向下增長的現象 (為了展示向下增長的現象,上圖將 stack 反過來畫)
在上圖我們執行完 `pushq` 之後,接著執行 `popq %rdx`。
執行 `pushq %rax` 時,首先 `rsp` 的值變成 `0x100`,接著將 `rax` 的值,也就是 `0x123` 放置到 `rsp` 所指向的記憶體地址,也就是將 `0x123` 放置到記憶體地址 `0x100`
接著執行 `popq %rax`,首先會將 `rsp` 中記憶體地址的值寫入到 `rax` 中,`rsp` 中記憶體地址為 `0x100`,`0x100` 指向 `0x123`,因此 `0x123` 會寫入到 `rax` 中,接著將 `rsp` 加 8。
可以發現到 `rsp` 總是指向到 stack 的頂部。
由於 stack 和程式的其他資訊,都是在程式執行的記憶體區域中,可以當作一個程式執行時,會變成一個 Process,一個 Process 會有一個 Memory Image,Process 可以存取這一個 Memory Image,我們可以在程式中通過記憶體操作存取到程式 stack 的資訊,假設 stack 中有兩個 4 word 的資料,我們可以直接通過 `movq 8(%rsp), %rdx` 將第二個 4 word 的資料複製到 `rdx` 暫存器中。
### 組合語言: leaq