程式編碼與資料格式 (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) 機器層級程式碼 兩大抽象:ISAvirtual 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=103、M=106、G=109):

處理器 (年份) 電晶體 對機器層級程式設計的重要性
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
Moore's Law (p.205)

Gordon Moore 於 1965 年依當時技術(單晶片約 64 個電晶體)預測晶片電晶體數每年翻倍(為期 10 年)。實際上半導體產業 50 多年來平均約每 18 個月翻倍(他的預測略嫌樂觀但也太短視);x86 系列實測年成長率約 37%,即約每 26 個月翻倍。磁碟與半導體記憶體容量亦呈類似指數成長,是電腦革命的主要驅動力。

為何轉向 64-bit (p.202)

32-bit 機器只能使用約 4 GB(232 bytes) RAM;記憶體價格下降、需求上升後此上限成為瓶頸。現行 64-bit 機器可用達 256 TB(248 bytes),架構上可擴充至 16 EB(264 bytes)

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)

3.2.1 機器層級程式碼 (Machine-Level Code) (p.206-208)

機器層級程式設計仰賴兩個關鍵抽象(呼應 1.9.3 節):

  1. ISA(instruction set architecture):定義處理器狀態、指令格式、與各指令對狀態的效果。多數 ISA(含 x86-64)把程式行為描述成指令循序執行(一條完成才開始下一條);實際硬體會並行執行多條指令,但以防護機制確保整體行為與 ISA 的循序語意一致(此落差見 04-Processor-Architecture/04-Pipelining-Principles05-Program-Optimization/02-Modern-Processor-Operation)。
  2. 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 整數         │
                                  └─────────────────────────────┘

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):

連結成可執行檔須有一個檔含 main(gcc -Og -o prog main.c mstore.c;prog 增至 8,655 bytes,因還包含啟動/終止程式與跟 OS 互動的程式碼)。objdump -d prog.o 的反組譯有三個差異(p.211):

  1. 位址被移動到不同範圍(如 0x400540 起)——linker 重新配置了程式碼位置。
  2. callq目標位址已填入(e8 42 00 00 00 → 呼叫 mult2)——linker 的任務之一是把函式呼叫對應到該函式可執行碼的位置(見 07-Linking/02-Symbol-Resolution-and-Relocation)。
  3. ret 之後多了兩條 nop(對程式無作用)——把函式碼填充到 16 bytes,讓下一段程式碼在記憶體系統效能上有更好的擺放位置。

3.2.3 格式注意事項 (Notes on Formatting) (p.211-213)

gcc 產生的 .s 檔對人類不易閱讀:一方面含有我們不需關心的資訊,另一方面又沒有任何關於程式如何運作的說明。

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
大小尾碼 pushqmovq pushmov(省略)
暫存器前綴 %rbx rbx(無 %)
記憶體位置寫法 (%rbx) QWORD PTR [rbx]
多運算元順序 movq %rdx, %rbx(src, dst) mov rbx, rdx(相反:dst, src)
運算元順序相反

ATT 與 Intel 格式的多運算元指令列出順序完全相反,在兩種格式間切換時極易混淆。閱讀 Intel/Microsoft 文件時務必注意。

在 C 中嵌入組合語言(Web Aside ASM:EASM, p.214)

某些機器功能 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
尾碼 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 現行實作? 232 = 4 GB;248 = 256 TB(架構可達 264 = 16 EB)
x86-64 虛擬位址的有效範圍 64-bit word 但高 16 bits 須為 0 → 可定址 248 bytes = 256 TB
誰最先推出 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 架構的歷史)
intlong、指標在 x86-64 的大小與尾碼 4 (l)、8 (q)、8 (q) bytes
ATT 與 Intel 格式最易錯的差異 多運算元順序相反(ATT: src, dst;Intel: dst, src)
.globl.text. 開頭行 給 assembler/linker 的 directives,閱讀時通常可忽略
為何 movll 不與 double 衝突 浮點使用完全不同的指令與暫存器集,無歧義
long double 為何不建議 80-bit x87 格式:不可攜、通常無高效能硬體實作
ISA 的循序語意 vs 實際硬體 ISA 描述指令循序執行;硬體並行執行多條指令,但保證整體行為等同循序