資訊表示與程式翻譯 (Information & Program Translation)
Overview Table
| 小節 | 主題 | 一句話重點 |
|---|---|---|
| 1.1 (p.39) | Information Is Bits + Context | 系統中一切資訊都是 bits;同一串 bytes 的意義完全取決於情境 (context) |
| 1.1 Aside (p.40) | C 語言起源 | C 由 Dennis Ritchie(Bell Labs, 1969–1973)開發,與 Unix 共生、小而簡單、為實用而生 |
| 1.2 (p.40-42) | 程式的翻譯 | hello.c 經 preprocessor → compiler → assembler → linker 四階段變成可執行檔 |
| 1.2 Aside (p.42) | GNU 專案 | gcc 來自 Stallman 1984 年創立的 GNU 專案;Linux 核心 + GNU 工具 = 完整系統 |
| 1.3 (p.42-43) | 為何要懂編譯系統 | 三大理由:最佳化效能、理解 link-time 錯誤、避免安全漏洞 |
1.1 資訊 = 位元 + 情境 (Information Is Bits + Context) (p.39)
hello 程式的生命始於程式設計師用編輯器建立的原始程式 (source program / source file) hello.c。原始程式本質上是一串 bit(值為 0 或 1),以 8 個 bit 為一組組織成 byte;每個 byte 代表程式中的某個文字字元。
- 多數系統以 ASCII 標準表示文字字元:每個字元對應一個唯一的 byte 大小整數值。
- 例:
hello.c第一個 byte 值為35→ 字元#;第二個 byte 值為105→ 字元i。 - 每行結尾都有肉眼看不見的 newline 字元
'\n',整數值為10。
- 例:
- text file(文字檔):內容全部由 ASCII 字元構成的檔案(如
hello.c)。 - binary file(二進位檔):所有其他檔案。
系統中所有資訊——磁碟檔案、記憶體中的程式與使用者資料、網路上傳輸的資料——都只是一堆 bits。唯一能區分不同資料物件的是我們檢視它們的情境 (context):同一串 bytes 在不同情境下,可能代表整數、浮點數、字串或機器指令。
- 程式設計師必須理解數值的機器表示法,因為它們不等於數學上的整數與實數:它們是有限的近似 (finite approximations),可能出現出乎意料的行為(細節在第 2 章,見 02-Information-Representation/01-Information-Storage-and-Bits)。
非英語文字使用其他編碼方法(如 Unicode/UTF-8,書中 p.86 的 aside 討論)。「每字元一 byte」的敘述僅對 ASCII 成立。
Aside:C 語言的起源 (p.40)
C 由 Dennis Ritchie(Bell Laboratories)於 1969–1973 年開發;ANSI 於 1989 年批准 ANSI C 標準,後由 ISO 接手。標準定義了 C 語言與 C standard library。K&R(Kernighan & Ritchie)的經典書描述 ANSI C。Ritchie 自評:C 是 "quirky, flawed, and an enormous success"。成功原因:
| 成功原因 | 說明 |
|---|---|
| 與 Unix 緊密相連 | C 一開始就是 Unix 的系統程式語言;Unix kernel 與其工具/函式庫幾乎全以 C 寫成,Unix 因此易於移植,又擴大了 C 的使用者群 |
| 小而簡單 | 設計由單一人主導而非委員會,乾淨一致;K&R 一書僅 261 頁即涵蓋完整語言與標準函式庫 |
| 為實用目的而設計 | C 為實作 Unix 而生;後來大家發現寫想寫的程式時語言不會礙事 |
- 缺點:pointer 是常見的混淆與錯誤來源;缺乏 class、object、exception 等抽象的原生支援(C++、Java 為應用層程式補足這些)。
1.2 程式被其他程式翻譯成不同形式 (p.40-42)
hello 以高階 C 程式的形式誕生(人類可讀),但要在系統上執行,必須由其他程式把每條 C 敘述翻譯成一連串低階機器語言指令 (machine-language instructions),再打包成可執行目的程式 (executable object program),以二進位磁碟檔的形式儲存(也稱 executable object files)。
在 Unix 系統上,由編譯器驅動程式 (compiler driver) 完成整個翻譯:
linux> gcc -o hello hello.c
執行四階段翻譯的程式(preprocessor、compiler、assembler、linker)合稱編譯系統 (compilation system):
printf.o
|
v
hello.c --> [cpp] --> hello.i --> [cc1] --> hello.s --> [as] --> hello.o --> [ld] --> hello
Pre- Compiler Assembler Linker
processor
Source | Modified | Assembly | Relocatable | Executable
program | source | program | object | object
(text) | program | (text) | programs | program
| (text) | | (binary) | (binary)
(Figure 1.3, p.41)
四階段比較表
| 階段 | 程式 | 輸入 → 輸出 | 檔案型態 | 做什麼 |
|---|---|---|---|---|
| Preprocessing | cpp |
hello.c → hello.i |
text → text | 依 # 開頭的 directive 修改原始 C 程式;如 #include <stdio.h> 會把系統標頭檔 stdio.h 的內容直接插入程式文字 |
| Compilation | cc1 |
hello.i → hello.s |
text → text | 翻譯成組合語言程式 (assembly-language program) |
| Assembly | as |
hello.s → hello.o |
text → binary | 翻譯成機器語言指令並打包為 relocatable object program;用文字編輯器打開會是亂碼 |
| Linking | ld |
hello.o + printf.o → hello |
binary → binary | 合併預編譯目的檔;產出可載入記憶體執行的 executable object file |
各階段細節(p.41-42):
- 組合語言的價值:為不同高階語言的不同編譯器提供共同的輸出語言——C 編譯器與 Fortran 編譯器產生相同組合語言格式的輸出。書中
hello.s的main定義:
main:
subq $8, %rsp
movl $.LC0, %edi
call puts
movl $0, %eax
addq $8, %rsp
ret
每一行對應一條低階機器指令的文字形式(深入見 03-Machine-Level-Programs/01-Program-Encodings-and-Data-Formats)。
- Assembly phase:
hello.o中main函式的指令僅佔 17 bytes(二進位編碼)。 - Linking phase:
hello呼叫的printf屬於每個 C 編譯器都提供的 standard C library,存在於獨立的預編譯目的檔printf.o中;linker (ld) 負責把它與hello.o合併,結果hello即可被載入記憶體並由系統執行(深入見 07-Linking/01-Static-Linking-and-Object-Files)。
.c(source, text)→ .i(modified source, text)→ .s(assembly, text)→ .o(relocatable object, binary)→ 無副檔名 executable(binary)。前三個是文字檔、後兩個是二進位檔;text/binary 的分界在 assembler。
gcc是 compiler driver(驅動四個階段),不是狹義的 compiler;狹義 compiler 是cc1。hello.o是 relocatable object file,不能直接執行;要經過 linking 才成為 executable。- 書中組語範例呼叫的是
puts而非printf——這是編譯器對「無格式化參數的 printf」做的最佳化,屬於一般化敘述的例外。
Aside:GNU 專案 (p.42)
gcc是 GNU(GNU's Not Unix)專案眾多工具之一。GNU 由 Richard Stallman 於 1984 年創立(免稅慈善組織),目標是開發一套原始碼不受修改/散布限制的完整類 Unix 系統。- GNU 環境包含 Unix 作業系統的所有主要元件——除了 kernel(由 Linux 專案另行開發):
emacs編輯器、gcc編譯器、gdb除錯器、assembler、linker、二進位處理工具等。 gcc支援多種語言(C、C++、Fortran、Java、Pascal、Objective-C、Ada)並能為多種機器產生程式碼。- 現代開源運動的思想根源是 GNU 的 free software 概念("free" as in "free speech," not "free beer");Linux 的流行很大程度歸功於 GNU 工具提供的環境。
1.3 理解編譯系統如何運作是值得的 (p.42-43)
對 hello.c 這種簡單程式,可以放心信任編譯系統產出正確且有效率的機器碼;但程式設計師仍需理解編譯系統運作的三大理由:
| 理由 | 核心問題(書中列舉) | 對應章節 |
|---|---|---|
| 最佳化程式效能 (Optimizing program performance) | switch 是否總比一連串 if-else 有效率?函式呼叫的 overhead 多大?while 是否比 for 快?pointer 存取是否比 array index 快?為何加總到區域變數比加總到 pass-by-reference 參數快得多?為何重排算式括號能讓函式變快? |
Ch.3 (x86-64 機器語言)、Ch.5(調校 C 程式)、Ch.6(記憶體階層與陣列儲存) |
| 理解 link-time 錯誤 (Understanding link-time errors) | linker 回報 unresolved reference 是什麼意思?static 變數與 global 變數的差別?兩個 C 檔定義同名 global 變數會怎樣?static library 與 dynamic library 的差別?為何命令列上函式庫的順序很重要?最可怕的:為何某些 linker 相關錯誤直到 run time 才出現? | Ch.7(見 07-Linking/02-Symbol-Resolution-and-Relocation) |
| 避免安全漏洞 (Avoiding security holes) | buffer overflow 漏洞多年來是網路/Internet 伺服器安全漏洞的主因,根源是程式設計師不了解必須嚴格限制從不可信來源接收資料的數量與形式;安全程式設計的第一步是理解資料與控制資訊在 program stack 上的儲存方式與後果 | Ch.3(stack discipline 與 buffer overflow,見 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety);並學習程式設計師/編譯器/OS 降低攻擊威脅的方法 |
- 不需要知道編譯器的內部運作也能寫出高效程式,但要做出好的 coding 決策,必須對機器層級程式碼、以及編譯器如何翻譯各種 C 敘述有基本理解 (p.42)。
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| 「同一串 bytes 可能是整數、浮點數、字串或指令」考什麼觀念? | Information is bits + context——區分資料物件的唯一因素是檢視的情境 |
hello.c 中字元 # 的 ASCII 值? |
35;'\n' 是 10,空白 SP 是 32 |
| 只含 ASCII 字元的檔案叫什麼?其他呢? | text file;其他一律是 binary file |
| 機器裡的數與數學的整數/實數關係? | 是有限近似 (finite approximations),可能有出乎意料的行為(Ch.2) |
| 編譯系統四階段依序為? | Preprocessing (cpp) → Compilation (cc1) → Assembly (as) → Linking (ld) |
hello.i / hello.s / hello.o 各是什麼? |
modified source(text)/ assembly program(text)/ relocatable object program(binary) |
#include <stdio.h> 由哪個階段處理?做什麼? |
Preprocessor (cpp):把 stdio.h 內容直接插入程式文字,輸出 .i 檔 |
| 為何組合語言「有用」? | 為不同高階語言(C、Fortran…)的編譯器提供共同輸出語言 |
printf 的程式碼從哪來?誰合併? |
位於 standard C library 的預編譯目的檔 printf.o;由 linker (ld) 與 hello.o 合併 |
.o 檔可以直接執行嗎? |
不行,relocatable object file 必須先 link 成 executable |
| 哪個工具是 compiler driver? | gcc(驅動 cpp、cc1、as、ld 全部四階段) |
| GNU 專案缺了哪個 Unix 元件? | kernel(由 Linux 專案開發);GNU 提供 emacs、gcc、gdb、assembler、linker 等 |
| 需要懂編譯系統的三大理由? | 最佳化效能、理解 link-time 錯誤、避免安全漏洞(buffer overflow) |
| buffer overflow 漏洞的根本原因? | 未嚴格限制從不可信來源接受資料的數量與形式;須理解 stack 上資料與控制資訊的儲存方式 |
Related Notes
- 01-Computer-Systems-Tour/02-Hardware-Organization-and-Program-Execution — 接續 1.4:執行檔如何被硬體載入與執行
- 01-Computer-Systems-Tour/03-OS-Abstractions-and-Networks — 作業系統如何抽象化本章提到的硬體資源
- 01-Computer-Systems-Tour/04-Amdahls-Law-and-Concurrency-Themes — 第 1 章後半的效能與並行主題
- 02-Information-Representation/01-Information-Storage-and-Bits — 「bits + context」與有限近似的完整展開
- 03-Machine-Level-Programs/01-Program-Encodings-and-Data-Formats — 組譯與機器碼編碼細節(gcc/objdump 實作)
- 03-Machine-Level-Programs/06-Buffer-Overflow-and-Pointer-Safety — 1.3 安全漏洞理由的深入章節
- 07-Linking/01-Static-Linking-and-Object-Files — linking phase 與 relocatable/executable object file 的深入章節
- 01-Computer-Systems-Tour/Practice-Computer-Systems-Tour — 本章練習題