靜態連結與目的檔 (Static Linking & Object Files)
Overview Table
| 主題 | 核心內容 | 書頁 |
|---|---|---|
| Compiler driver | gcc 依序呼叫 cpp → cc1 → as → ld,把 .c 變成可執行檔 |
p.707-708 |
| Static linking | 靜態連結器 (ld) 的兩大任務:symbol resolution + relocation |
p.708-709 |
| 目的檔三種形式 | relocatable / executable / shared object file | p.709 |
| 目的檔格式 | Linux 用 ELF;Windows 用 PE、Mac OS-X 用 Mach-O、早期 Unix 用 a.out | p.709 |
| ELF relocatable 結構 | ELF header + sections(.text、.data、.bss…)+ section header table |
p.710-711 |
| Linker symbol 三類 | 本模組定義的 global、外部定義的 external、僅本模組可見的 local(static) |
p.711-712 |
| Symbol table entry | Elf64_Symbol:name / type / binding / section / value / size |
p.712-713 |
| Pseudosections | ABS、UNDEF、COMMON(只存在於 relocatable object file) | p.713 |
| COMMON vs .bss | 未初始化 global → COMMON;未初始化 static、初始化為 0 者 → .bss |
p.713 |
本章以 x86-64 Linux + ELF-64 為具體情境,但連結的基本概念跨 OS、ISA、目的檔格式皆通用——細節不同、概念相同。
7.1 Compiler Drivers(編譯器驅動程式)(p.707-708)
Compiler driver(如 gcc)代替使用者依序呼叫 preprocessor(cpp)、compiler(cc1)、assembler(as)、linker(ld)。一行 gcc -Og -o prog main.c sum.c 背後是四個階段的轉換,可用 gcc -v 觀察全過程。
main.c ──cpp──▶ main.i ──cc1──▶ main.s ──as──▶ main.o ─┐
(C 原始碼) (預處理後 (組合語言 (relocatable │
ASCII) ASCII) object) ├──ld──▶ prog
sum.c ──cpp──▶ sum.i ──cc1──▶ sum.s ──as──▶ sum.o ─┘ (+ 系統目的檔)
(executable
object file)
cpp [args] main.c /tmp/main.i:preprocessor 把main.c翻成 ASCII 中間檔main.icc1 /tmp/main.i -Og [args] -o /tmp/main.s:compiler 產生 ASCII 組合語言檔main.sas [args] -o /tmp/main.o /tmp/main.s:assembler 產生二進位 relocatable object filemain.old -o prog [system object files] /tmp/main.o /tmp/sum.o:linker 合併多個.o與必要的系統目的檔,產生 executable object fileprog- 執行
./prog時,shell 呼叫 OS 的 loader,把prog的 code 與 data 複製進記憶體,再把控制權轉移到程式開頭
某些版本的 gcc 把 preprocessor 整合進 compiler driver(p.707 註 1),未必能看到獨立的 cpp 步驟——但概念上四階段不變。編譯系統概觀另見 01-Computer-Systems-Tour/01-Information-and-Program-Translation。
7.2 Static Linking(靜態連結)(p.708-709)
Static linker(Linux 的 ld)輸入是一組 relocatable object files 加上命令列參數,輸出是可載入執行的 fully linked executable object file。輸入檔由各種 code/data sections 組成,每個 section 是一段連續的位元組序列——指令一區、已初始化全域變數一區、未初始化變數又一區。
連結器必須完成兩大任務:
| 步驟 | 任務 | 內容 |
|---|---|---|
| Step 1 | Symbol resolution(符號解析) | 目的檔會定義與參考 symbols(函式、全域變數、static 變數)。目標:把每個 symbol reference 對應到恰好一個 symbol definition |
| Step 2 | Relocation(重定位) | compiler/assembler 產生的 code/data sections 都從位址 0 開始。連結器為每個 symbol definition 指定記憶體位址,再修改所有參考它的位置。依據 assembler 產生的 relocation entries 機械式 (blindly) 執行 |
關於連結器的基本認知(p.709):
- 目的檔只是位元組區塊 (blocks of bytes) 的集合:有的裝程式碼、有的裝資料、有的裝引導 linker/loader 的資料結構
- 連結器的工作就是串接 (concatenate) 區塊、決定執行期位置、修改 code/data 區塊內的若干位置
- 連結器對目標機器的理解極少——大部分工作已由 compiler 與 assembler 完成
兩大任務的細節(strong/weak 符號規則、relocation 演算法)在 07-Linking/02-Symbol-Resolution-and-Relocation 展開;本筆記聚焦目的檔格式與符號表本身。
7.3 Object Files(目的檔三種形式)(p.709)
| 形式 | 內容 | 由誰產生 |
|---|---|---|
| Relocatable object file | 二進位 code + data,可在編譯期與其他 relocatable object files 合併,產生 executable | compiler + assembler |
| Executable object file | 二進位 code + data,可直接複製進記憶體並執行 | linker |
| Shared object file | 特殊的 relocatable object file,可在載入期 (load time) 或執行期 (run time) 動態載入與連結 | compiler + assembler |
- 技術上,object module 是一串位元組序列,object file 是存在磁碟上的 object module——書中兩詞交替使用
- 目的檔格式隨系統而異:早期 Bell Labs Unix 用 a.out(至今執行檔仍俗稱 a.out);Windows 用 PE(Portable Executable);Mac OS-X 用 Mach-O;現代 x86-64 Linux/Unix 用 ELF(Executable and Linkable Format)
Shared object file 的動態連結細節(shared libraries、dlopen)見 07-Linking/03-Executable-Loading-and-Dynamic-Linking。
7.4 Relocatable Object Files(ELF 重定位目的檔)(p.710-711)
ELF header 開頭是 16-byte 序列,描述產生此檔系統的 word size 與 byte ordering;其餘欄位包含 ELF header 大小、目的檔型別(relocatable/executable/shared)、機器型別(如 x86-64)、section header table 的檔案偏移量及其項目大小與數量。Section header table 為目的檔中每個 section 存放一筆固定大小的 entry,描述各 section 的位置與大小。Sections 本體夾在 ELF header 與 section header table 之間。
偏移 0 ┌──────────────────────┐
│ ELF header │ ◀─ word size、byte order、檔案型別、
├──────────────────────┤ machine type、section header table 位置
│ .text │ ─┐
│ .rodata │ │
│ .data │ │
│ .bss │ │
│ .symtab │ ├─ Sections
│ .rel.text │ │
│ .rel.data │ │
│ .debug │ │
│ .line │ │
│ .strtab │ ─┘
├──────────────────────┤
│ Section header table │ ◀─ 描述各 section 的位置與大小
└──────────────────────┘ (每 section 一筆固定大小 entry)
| Section | 內容 | 備註 |
|---|---|---|
.text |
編譯後程式的機器碼 | |
.rodata |
唯讀資料:如 printf 的格式字串、switch 的 jump tables |
|
.data |
已初始化的 global 與 static C 變數 | local 變數在執行期於 stack 上維護,不出現在 .data 或 .bss |
.bss |
未初始化的 global/static 變數,以及初始化為 0 的 global/static 變數 | 不占目的檔實際空間,只是佔位符 (placeholder);執行期配置記憶體並初始化為 0 |
.symtab |
符號表:程式中定義與參考的函式、全域變數資訊 | 不需要 -g 就存在;不含 local 變數 entry |
.rel.text |
.text 中連結時需修改的位置清單 |
呼叫 external function 或參考 global variable 的指令需修改;呼叫 local function 的不需要;executable object file 通常省略 relocation 資訊(除非明確要求保留) |
.rel.data |
模組參考或定義之 global 變數的 relocation 資訊 | 初始值為「某 global 變數或外部函式位址」的已初始化全域變數需修改 |
.debug |
除錯符號表:local 變數、typedefs、global 變數、原始 C 檔 | 只有 -g 才存在 |
.line |
原始 C 程式行號 ↔ .text 機器碼指令的對映 |
只有 -g 才存在 |
.strtab |
字串表:給 .symtab、.debug 的符號名與 section headers 的 section 名 |
一連串 null-terminated 字串 |
源自 IBM 704 組合語言(約 1957)的 "Block Started by Symbol" 指示詞。助記法:"Better Save Space!"——未初始化變數不必占磁碟空間,這正是目的檔格式區分已初始化/未初始化變數的空間效率理由。
「要有 -g 才有符號表」是錯的:每個 relocatable object file 都有 .symtab(除非用 strip 指令移除);-g 影響的是 .debug 與 .line。但 .symtab 與 compiler 內部符號表不同——不含 local 非 static 變數的 entry。
7.5 Symbols and Symbol Tables(符號與符號表)(p.711-714)
每個 relocatable object module m 都有一個符號表,記錄 m 所定義與參考的符號。對連結器而言符號分三類:
| 符號種類 | 定義 | 對應的 C 實體 |
|---|---|---|
| Global symbols(由 m 定義) | m 定義、可被其他模組參考 | 非 static 的 C 函式與 global 變數 |
| Externals | m 參考、但由其他模組定義的 global symbols | 定義在其他模組的非 static 函式與 global 變數 |
| Local symbols | m 獨占定義與參考;模組內任何地方可見,其他模組不可參考 | static C 函式與帶 static 屬性的全域變數 |
- Local linker symbol ≠ local 程式變數:
.symtab不含 local 非 static 變數的符號——它們在執行期由 stack 管理(見 03-Machine-Level-Programs/04-Procedures-and-the-Stack),連結器不關心 - static local 變數不在 stack 上:compiler 在
.data或.bss為每個定義配置空間,並在符號表建立唯一名稱的 local linker symbol。例:f()與g()各定義static int x,compiler 可能對前者輸出x.1、後者輸出x.2(p.712) - 符號表由 assembler 建立,使用 compiler 輸出到
.s組合語言檔的符號;ELF 符號表放在.symtabsection,內容是一個 entry 陣列
C 用 static 把變數/函式藏在模組內,類似 Java/C++ 的 public/private:C 中原始檔就是模組;帶 static 的全域宣告為該模組私有,不帶的則是 public、任何模組可存取。好習慣:能加 static 就加。
ELF 符號表 entry:Elf64_Symbol(p.713, Figure 7.4)
typedef struct {
int name; /* String table offset */
char type:4, /* Function or data (4 bits) */
binding:4; /* Local or global (4 bits) */
char reserved; /* Unused */
short section; /* Section header index */
long value; /* Section offset or absolute address */
long size; /* Object size in bytes */
} Elf64_Symbol;
| 欄位 | 意義 |
|---|---|
name |
指向 string table 的 byte offset(null-terminated 符號名) |
value |
符號位址:relocatable module 中 = 所屬 section 起點的 offset;executable object file 中 = 絕對執行期位址 |
size |
物件大小(bytes) |
type(4 bits) |
通常是 data 或 function;也有 section entry、原始檔路徑等其他類型 |
binding(4 bits) |
符號是 local 或 global |
section |
符號所屬 section,為 section header table 的索引 |
三個 pseudosections(p.713)
沒有 section header table entry 的特殊 section;只存在於 relocatable object file,executable object file 中沒有:
| Pseudosection | 意義 | 特殊欄位語意 |
|---|---|---|
| ABS | 不應被 relocate 的符號 | — |
| UNDEF | 未定義符號:此模組參考、他處定義 | — |
| COMMON | 尚未配置 (not yet allocated) 的未初始化資料物件 | value = 對齊需求 (alignment);size = 最小大小 |
COMMON vs .bss(現代 gcc 慣例,p.713)
| 分類 | 去處 |
|---|---|
| 未初始化的 global 變數 | COMMON |
| 未初始化的 static 變數;初始化為 0 的 global 或 static 變數 | .bss |
這個看似武斷的區分源於連結器做 symbol resolution 的方式(strong/weak symbol 規則,7.6 節),詳見 07-Linking/02-Symbol-Resolution-and-Relocation。
用 readelf 觀察符號表(p.714)
GNU readelf 是檢視目的檔內容的利器。main.o(範例程式 main.c/sum.c,Figure 7.1)最後三筆符號表 entry(前 8 筆是連結器內部使用的 local 符號):
Num: Value Size Type Bind Vis Ndx Name
8: 0000000000000000 24 FUNC GLOBAL DEFAULT 1 main
9: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
main:global 符號定義,24-byte FUNC,位於.text(Ndx=1)offset 0array:global 符號定義,8-byte OBJECT,位於.data(Ndx=3)offset 0sum:對外部符號的參考,Ndx=UND(未定義)- readelf 以整數索引標識 section:Ndx=1 是
.text,Ndx=3 是.data
銜接:符號解析的難點(p.715 開頭)
- Local symbol 的解析很直接:compiler 保證每模組每個 local symbol 只有一個定義,且 static local 變數的 linker symbol 名稱唯一
- Global symbol 較棘手:compiler 遇到當前模組未定義的符號時,假設它定義在別的模組,產生 linker symbol table entry 交給連結器處理。連結器在所有輸入模組都找不到定義就印出(常常很難懂的)錯誤訊息並終止,如
undefined reference to 'foo'——compile 會過、link 才炸 - 多個模組定義同名 global symbol 時,連結器必須報錯或挑一個定義捨棄其餘——Linux 由 compiler、assembler、linker 合作處理,可能引入令人困惑的 bug(strong/weak 規則見 07-Linking/02-Symbol-Resolution-and-Relocation)
Exam/Test Patterns
| 情境 / 關鍵字 | 答案 |
|---|---|
| 「連結器的兩大任務?」 | Symbol resolution(每個 reference ↔ 恰好一個 definition)+ Relocation(section 從 0 開始 → 指定執行期位址並修改所有參考) |
| 「gcc 編譯四階段與工具?」 | cpp(.c→.i)→ cc1(.i→.s)→ as(.s→.o)→ ld(.o 們→executable);用 gcc -v 觀察 |
| 「目的檔三種形式,各由誰產生?」 | relocatable、executable、shared;compiler/assembler 產生 relocatable(含 shared),linker 產生 executable |
| 「已初始化 global 變數放哪?未初始化呢?」 | 已初始化 → .data;未初始化 global → COMMON;未初始化 static / 初始化為 0 → .bss |
「switch 的 jump table、printf 格式字串放哪?」 |
.rodata |
「local 變數在 .symtab 有 entry 嗎?」 |
沒有(如 Problem 7.1 的 temp)——由 stack 管理;但 static local 變數有(配置在 .data/.bss,唯一名稱如 x.1、x.2) |
「.bss 為何不占檔案空間?」 |
只是 placeholder;執行期才配置記憶體並清 0(Better Save Space) |
| 「relocatable vs executable 的 symbol value 差異?」 | relocatable:section 內 offset;executable:絕對執行期位址 |
| 「Ndx = UND / ABS / COMMON 意義?」 | UNDEF:此模組參考、他處定義;ABS:不做 relocate;COMMON:未配置的未初始化資料(value=對齊需求、size=最小大小);三者只在 relocatable object file 存在 |
| 「呼叫 local function 的指令需要 relocation entry 嗎?」 | 不用;呼叫 external function 或參考 global 變數的才要(記在 .rel.text) |
「extern int buf[]; int *bufp0 = &buf[0]; int *bufp1; 各符號分類?」(Problem 7.1 型) |
buf=extern(m.o 定義,.data);bufp0=global/.data(已初始化);bufp1=global/COMMON(未初始化 global);swap=global/.text;temp=無 entry |
「compile 成功但出現 undefined reference 是誰的錯誤?」 |
linker 階段:compiler 假設符號定義在他模組並留給 linker;linker 找不到定義才報錯終止 |
「沒加 -g,object file 有符號表嗎?」 |
有,.symtab 一律存在(除非 strip);-g 只影響 .debug 與 .line |
Related Notes
- 07-Linking/02-Symbol-Resolution-and-Relocation
- 07-Linking/03-Executable-Loading-and-Dynamic-Linking
- 07-Linking/04-Library-Interpositioning-and-Tools
- 07-Linking/Practice-Linking
- 01-Computer-Systems-Tour/01-Information-and-Program-Translation
- 03-Machine-Level-Programs/01-Program-Encodings-and-Data-Formats
- 09-Virtual-Memory/04-Memory-Mapping