程式編碼與資料格式 (Program Encodings & Data Formats)
Overview Table
| 小節 | 主題 | 核心重點 |
|---|---|---|
| 3.1 (p.202-205) | 歷史觀點 | 8086 → Core i7 Haswell;backward compatible;IA32 → x86-64(由 AMD 首創);Moore's Law |
| 3.2 (p.205-206) | 程式編碼 | gcc -Og 流程:preprocessor → compiler → assembler → linker;object vs executable code |
| 3.2.1 (p.206-208) | 機器層級程式碼 | 兩大抽象:ISA 與 virtual address;PC(%rip)、register file、condition codes、vector registers |
| 3.2.2 (p.208-211) | 程式碼範例 | -S 產生 .s、-c 產生 .o;objdump -d 反組譯;指令長 1–15 bytes、唯一解碼 |
| 3.2.3 (p.211-213) | 格式注意事項 | . 開頭為 assembler/linker directives;ATT vs Intel 格式差異 |
| 3.3 (p.213-215) | 資料格式 | Intel「word」= 16 bits;指令尾碼 b/w/l/q 對應 1/2/4/8 bytes |
3.1 歷史觀點 (A Historical Perspective) (p.202-205)
Intel 處理器產品線俗稱 x86,自 1978 年 8086(最早的單晶片 16-bit 微處理器之一)一路演進至今。每一代都設計成 backward compatible(向後相容)——能執行為任何較早版本編譯的程式碼,因此指令集中留有許多演化遺跡(strange artifacts)。Intel 曾用 IA32(Intel Architecture 32-bit)與 Intel64 等名稱;本書統稱其 64-bit 擴充為 x86-64,整條產品線沿用 i486 之前的命名慣例俗稱「x86」。
關鍵里程碑(以電晶體數衡量複雜度;K=
| 處理器 (年份) | 電晶體 | 對機器層級程式設計的重要性 |
|---|---|---|
| 8086 (1978) | 29 K | 16-bit;8088 變體為初代 IBM PC 核心;位址僅 20 bits(可定址 1,048,576 bytes);1980 年 8087 副處理器建立「x87」浮點模型 |
| 80286 (1982) | 134 K | 新增(現已過時)定址模式;IBM PC-AT / 初代 MS Windows 平台 |
| i386 (1985) | 275 K | 架構擴充到 32 bits;引入 Linux 與近代 Windows 使用的 flat addressing model;首台可完整支援 Unix |
| i486 (1989) | 1.2 M | 浮點單元整合進處理器晶片;指令集無大變動 |
| Pentium (1993) | 3.1 M | 效能提升,指令集僅小幅擴充 |
| PentiumPro (1995) | 5.5 M | 全新 P6 microarchitecture;新增 conditional move 指令類 |
| Pentium/MMX (1997) | 4.5 M | 整數向量指令(每筆 1/2/4 bytes,向量共 64 bits) |
| Pentium III (1999) | 8.2 M | 引入 SSE:整數或浮點向量,向量 128 bits;後期版本整合 L2 cache 達 24 M |
| Pentium 4 (2000) | 42 M | SSE2 新增 double-precision 等型別 + 144 條新指令;編譯器可改用 SSE 而非 x87 編譯浮點碼 |
| Pentium 4E (2004) | 125 M | 加入 hyperthreading;EM64T = Intel 對 AMD 開發之 64-bit 擴充(x86-64)的實作 |
| Core 2 (2006) | 291 M | 回歸類 P6 微架構;Intel 首款 multi-core(不支援 hyperthreading) |
| Core i7 Nehalem (2008) | 781 M | 同時具備 hyperthreading 與 multi-core(每核 2 程式、每晶片最多 4 核) |
| Sandy Bridge (2011) | 1.17 G | 引入 AVX(SSE 擴充至 256-bit 向量) |
| Haswell (2013) | 1.4 G | AVX2:更多指令與指令格式 |
x86 演進時間軸
1978 1985 1995 2000 2004 2006 2008~2013
8086 ──▶ i386 ──▶ PentiumPro ─▶ Pentium 4 ─▶ Pentium 4E ─▶ Core 2 ─▶ Core i7
16-bit 32-bit P6/cmov SSE2 x86-64 multi- Nehalem/Sandy
hyperthread core Bridge/Haswell
Gordon Moore 於 1965 年依當時技術(單晶片約 64 個電晶體)預測晶片電晶體數每年翻倍(為期 10 年)。實際上半導體產業 50 多年來平均約每 18 個月翻倍(他的預測略嫌樂觀但也太短視);x86 系列實測年成長率約 37%,即約每 26 個月翻倍。磁碟與半導體記憶體容量亦呈類似指數成長,是電腦革命的主要驅動力。
- AMD 長年生產與 Intel 相容(可執行完全相同機器碼)的處理器;約 2002 年起更具競爭力——率先突破 1 GHz 商用時脈,並率先推出 x86-64(被廣泛採納的 IA32 64-bit 擴充)。本章內容同樣適用於相容處理器。
- 對 Linux + gcc 使用者而言,x86 大部分歷史包袱無關緊要:8086/80286 的記憶體模型自 i386 起過時;x87 浮點指令自 SSE2 起過時。x86-64 程式中僅殘留少許歷史痕跡,最晦澀的特性多已不出現。
- IA32 仍是重要的機器語言(舊系統、向後相容模式),具備 x86-64 背景可輕鬆上手(Web Aside ASM:IA32, p.202)。
32-bit 機器只能使用約 4 GB(
3.2 程式編碼 (Program Encodings) (p.205-206)
linux> gcc -Og -o p p1.c p2.c 會啟動一整串程式,把原始碼轉為可執行碼(gcc 為 Linux 預設編譯器,亦可用 cc 呼叫)。-Og 讓機器碼遵循原始 C 程式的整體結構,適合學習;實務上 -O1/-O2 等較高等級效能較佳,但機器碼與原始碼的對應會難以理解。
-Og 的可用性(Simplification-with-exceptions)
-Og 為 gcc 4.8 引入;更舊版 gcc 或非 GNU 編譯器不認得此選項,應改用 -O1(p.206 註 1)。同理,不同編譯器/版本/選項產生的程式碼都會不同(Aside, p.208)——本書範例的目的在示範「如何檢視組合碼並對映回高階語言結構」,需自行適應你的編譯器風格。
gcc 驅動的完整編譯流程(p.206)
p1.c, p2.c 可執行檔
(source) ──▶ [C preprocessor] ──▶ [compiler] ──▶ [assembler] ──▶ [linker] ──▶ p
展開 #include p1.s, p2.s p1.o, p2.o 合併 .o 檔 +
與 #define 巨集 (assembly) (object code) 函式庫碼(如 printf)
- Object code(機器碼的第一種形式):含所有指令的二進位表示,但全域值的位址尚未填入。
- Executable code(第二種形式):處理器實際執行的確切形式;與連結流程的細節見第 7 章 07-Linking/01-Static-Linking-and-Object-Files。
3.2.1 機器層級程式碼 (Machine-Level Code) (p.206-208)
機器層級程式設計仰賴兩個關鍵抽象(呼應 1.9.3 節):
- ISA(instruction set architecture):定義處理器狀態、指令格式、與各指令對狀態的效果。多數 ISA(含 x86-64)把程式行為描述成指令循序執行(一條完成才開始下一條);實際硬體會並行執行多條指令,但以防護機制確保整體行為與 ISA 的循序語意一致(此落差見 04-Processor-Architecture/04-Pipelining-Principles 與 05-Program-Optimization/02-Modern-Processor-Operation)。
- Virtual address:機器層級程式使用的記憶體位址是虛擬位址,呈現為一個巨大的 byte-addressable 陣列;實際由多層硬體記憶體與 OS 軟體共同實作(第 9 章)。
x86-64 機器碼中可見、但 C 程式設計師平常看不到的處理器狀態:
| 處理器狀態 | 說明 |
|---|---|
| Program counter (PC) | x86-64 中稱 %rip;指出下一條待執行指令的記憶體位址 |
| Integer register file | 16 個具名 64-bit 暫存器;可存位址(對應 C 指標)或整數資料;部分追蹤程式狀態關鍵部位,其餘存暫時資料(參數、區域變數、回傳值) |
| Condition code registers | 保存最近一次算術/邏輯指令的狀態資訊;用以實作 if/while 等條件性控制或資料流變化 |
| Vector registers | 每個可存放一或多個整數或浮點值 |
C 程式設計師的視角 機器碼的視角
┌────────────────────────┐ ┌─────────────────────────────┐
│ 型別化物件: int, long, │ │ 記憶體 = 大型 byte 陣列 │
│ 指標, array, struct │ ──▶ │ (virtual addresses) │
│ 變數名稱、型別檢查 │ │ array/struct = 連續位元組 │
└────────────────────────┘ │ 不分 signed/unsigned、 │
│ 指標型別、指標 vs 整數 │
└─────────────────────────────┘
- 程式記憶體包含:executable machine code、OS 所需資訊、管理程序呼叫/返回的 run-time stack、使用者配置的記憶體區塊(如
malloc)。 - x86-64 虛擬位址以 64-bit words 表示,但現行實作要求高 16 bits 必須為 0,故位址實際可指定
bytes = 256 TB 範圍內的位元組;典型程式只會用到數 MB 至數 GB。OS 管理此虛擬位址空間,將虛擬位址轉譯為實體位址(見 09-Virtual-Memory/03-Address-Translation-and-TLB)。 - 單一機器指令只做極基本操作(如兩暫存器相加、記憶體↔暫存器搬移、條件跳轉至新指令位址);編譯器須產生指令序列來實作算術運算式求值、迴圈、程序呼叫/返回等結構。
3.2.2 程式碼範例 (Code Examples) (p.208-211)
以 mstore.c 中的函式為例:
long mult2(long, long);
void multstore(long x, long y, long *dest) {
long t = mult2(x, y);
*dest = t;
}
| 指令 | 產出 | 說明 |
|---|---|---|
gcc -Og -S mstore.c |
mstore.s |
只跑到編譯器就停,產生組合語言檔(可讀文字) |
gcc -Og -c mstore.c |
mstore.o |
編譯 + 組譯,產生二進位 object file(無法直接檢視) |
objdump -d mstore.o |
(輸出) | disassembler 由機器碼還原類組語格式 |
(gdb) x/14xb multstore |
(輸出) | 用 gdb 顯示函式起始位址開始的 14 個 hex 位元組(Aside, p.209) |
multstore 的組合碼中每個縮排行對應一條機器指令,區域變數名與資料型別資訊已全部剝除。1,368 bytes 的 mstore.o 內嵌 14-byte 機器碼序列 53 48 89 d3 e8 00 00 00 00 48 89 03 5b c3,反組譯如下:
Offset Bytes Assembly
0: 53 push %rbx
1: 48 89 d3 mov %rdx,%rbx
4: e8 00 00 00 00 callq 9 <multstore+0x9> ← 目標位址尚未填(.o 檔)
9: 48 89 03 mov %rax,(%rbx)
c: 5b pop %rbx
d: c3 retq
機器碼與其反組譯表示的重要性質(p.210):
- x86-64 指令長度 1 到 15 bytes;編碼設計讓常用、運算元少的指令佔較少位元組。
- 指令格式保證從固定起點有唯一解碼(unique decoding)——例如只有
pushq %rbx能以 byte 值53開頭。 - Disassembler 只靠機器碼檔中的位元組序列決定組合碼,不需原始碼或
.s檔。 - Disassembler 的命名慣例與 gcc 略異:常省略
q尾碼(size designator,多數情況可省略),卻替call/ret加上q(亦可安全省略)。 - 核心教訓:機器執行的程式就只是一串編碼指令的位元組序列,機器對產生這些指令的原始碼幾乎一無所知。
連結成可執行檔須有一個檔含 main(gcc -Og -o prog main.c mstore.c;prog 增至 8,655 bytes,因還包含啟動/終止程式與跟 OS 互動的程式碼)。objdump -d prog 與 .o 的反組譯有三個差異(p.211):
- 位址被移動到不同範圍(如
0x400540起)——linker 重新配置了程式碼位置。 callq的目標位址已填入(e8 42 00 00 00→ 呼叫mult2)——linker 的任務之一是把函式呼叫對應到該函式可執行碼的位置(見 07-Linking/02-Symbol-Resolution-and-Relocation)。ret之後多了兩條nop(對程式無作用)——把函式碼填充到 16 bytes,讓下一段程式碼在記憶體系統效能上有更好的擺放位置。
3.2.3 格式注意事項 (Notes on Formatting) (p.211-213)
gcc 產生的 .s 檔對人類不易閱讀:一方面含有我們不需關心的資訊,另一方面又沒有任何關於程式如何運作的說明。
- 所有以
.開頭的行是給 assembler 與 linker 的 directives(如.file、.text、.globl、.type、.size、.section),通常可忽略。 - 本書採風格化呈現:省略多數 directives、加上行號與註解,並標明參數所在暫存器(如
x in %rdi, y in %rsi, dest in %rdx)——這也是組語程式設計師常用的格式。
ATT vs Intel 組合語言格式(Aside, p.213):本書使用 ATT format(得名於 AT&T;gcc、objdump 等工具的預設);Microsoft 工具與 Intel 文件使用 Intel format(可用 gcc -Og -S -masm=intel mstore.c 產生)。
| 差異點 | ATT(本書) | Intel |
|---|---|---|
| 大小尾碼 | pushq、movq |
push、mov(省略) |
| 暫存器前綴 | %rbx |
rbx(無 %) |
| 記憶體位置寫法 | (%rbx) |
QWORD PTR [rbx] |
| 多運算元順序 | movq %rdx, %rbx(src, dst) |
mov rbx, rdx(相反:dst, src) |
ATT 與 Intel 格式的多運算元指令列出順序完全相反,在兩種格式間切換時極易混淆。閱讀 Intel/Microsoft 文件時務必注意。
某些機器功能 C 無法存取——例如 x86-64 每次算術/邏輯運算都會設定 1-bit condition code PF(parity flag):結果低 8 bits 含偶數個 1 時設為 1,否則為 0;C 要算出同樣資訊至少需七個位移/遮罩/XOR 運算,且 C 程式無法讀取 PF。解法有二:(1) 整個函式以組語撰寫,交由 assembler/linker 與 C 程式碼合併;(2) 用 gcc 的 inline assembly(asm directive)嵌入短小組語段,機器相依碼最少。代價是程式碼綁定特定機器類別(如 x86-64),僅在該功能只能如此取得時使用。
3.3 資料格式 (Data Formats) (p.213-215)
因 x86 起源於 16-bit 架構、再擴充為 32-bit,Intel 術語中 「word」= 16-bit 資料型別;據此 32-bit 稱 double word、64-bit 稱 quad word。C 原始型別在 x86-64 的對應(Figure 3.1, p.214):
| C 宣告 | Intel 資料型別 | 組語尾碼 | 大小 (bytes) |
|---|---|---|---|
char |
Byte | b |
1 |
short |
Word | w |
2 |
int |
Double word | l |
4 |
long |
Quad word | q |
8 |
char * |
Quad word | q |
8 |
float |
Single precision | s |
4 |
double |
Double precision | l |
8 |
- 指標(任何型別,表中以
char *代表)在 64-bit 機器上都是 8-byte quad word;long以 64 bits 實作,數值範圍極廣。本章多數範例操作指標與long(即 quad words),但指令集對 byte、word、double word 也有完整指令支援。 - 多數 gcc 產生的指令有單字元尾碼標示運算元大小:如資料搬移指令有四種變體——
movb(move byte)、movw(move word)、movl(move double word)、movq(move quad word)。 - 尾碼
l用於 double word,因 32-bit 量歷史上被視為「long word」。 - 浮點數兩種主要格式:single precision(4 bytes,對應
float) 與 double precision(8 bytes,對應double)——即 02-Information-Representation/04-Floating-Point 的 IEEE 754 格式。
l 的雙重意義 & long double 的例外
尾碼 l 同時表示 4-byte 整數與 8-byte double-precision 浮點數——但不會產生歧義,因為浮點碼使用完全不同的指令與暫存器集(見 03-Machine-Level-Programs/07-Floating-Point-Code)。
另外,x86 家族歷史上以特殊 80-bit(10-byte)x87 浮點格式實作所有浮點運算,C 中可用 long double 宣告;本書不建議使用——不可攜到其他機器類別,且通常沒有 single/double precision 那樣的高效能硬體實作(p.214-215)。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| 32-bit 機器 RAM 上限?64-bit 現行實作? | |
| x86-64 虛擬位址的有效範圍 | 64-bit word 但高 16 bits 須為 0 → 可定址 |
| 誰最先推出 x86-64? | AMD(Intel 的實作稱 EM64T,2004 Pentium 4E 加入);AMD 也率先突破 1 GHz |
| Moore's Law 的實際數字 | Moore 原預測每年翻倍;產業實際平均約每 18 個月翻倍;x86 系列約 37%/年 ≈ 每 26 個月翻倍 |
-Og 的用途與例外 |
產生遵循原始 C 結構的機器碼(學習用);gcc < 4.8 不支援,改用 -O1;-O1/-O2 效能較佳 |
-S / -c / -o / -d |
-S → .s 組語;-c → .o object 檔;-o 指定輸出名;objdump -d 反組譯 |
| Object code vs executable code | 前者全域值位址未填;後者為處理器執行的確切形式(linker 產生) |
| 機器碼可見、C 不可見的處理器狀態 | PC(%rip)、16 個 64-bit 整數暫存器、condition codes、vector registers |
| x86-64 指令長度 | 1–15 bytes;常用/少運算元者較短;從固定起點有唯一解碼(如 53 只能是 pushq %rbx) |
| 連結後反組譯的三個差異 | 位址被移動、callq 目標位址已填入、補 nop 使函式碼滿 16 bytes(利記憶體系統效能) |
ret 後面的 nop 是做什麼? |
無執行效果的填充,改善下一塊程式碼的記憶體擺放 |
| Intel 的 word / double word / quad word | 16 / 32 / 64 bits(源自 16-bit 架構的歷史) |
int、long、指標在 x86-64 的大小與尾碼 |
4 (l)、8 (q)、8 (q) bytes |
| ATT 與 Intel 格式最易錯的差異 | 多運算元順序相反(ATT: src, dst;Intel: dst, src) |
.globl、.text 等 . 開頭行 |
給 assembler/linker 的 directives,閱讀時通常可忽略 |
為何 movl 的 l 不與 double 衝突 |
浮點使用完全不同的指令與暫存器集,無歧義 |
long double 為何不建議 |
80-bit x87 格式:不可攜、通常無高效能硬體實作 |
| ISA 的循序語意 vs 實際硬體 | ISA 描述指令循序執行;硬體並行執行多條指令,但保證整體行為等同循序 |
Related Notes
- 03-Machine-Level-Programs/02-Data-Movement-and-Arithmetic — 下一節:暫存器、operand 形式與 mov 系列指令
- 03-Machine-Level-Programs/07-Floating-Point-Code — x87 → SSE/AVX 浮點程式碼的完整討論
- 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety — 機器層級視角下的記憶體越界問題
- 01-Computer-Systems-Tour/01-Information-and-Program-Translation — 第 1 章的編譯系統總覽
- 02-Information-Representation/04-Floating-Point — single/double precision 的 IEEE 754 編碼
- 07-Linking/01-Static-Linking-and-Object-Files — object file 與 linker 的完整機制
- 09-Virtual-Memory/01-Address-Spaces-and-VM-Caching — 虛擬位址空間的實作