靜態連結與目的檔 (Static Linking & Object Files)

Overview Table

主題 核心內容 書頁
Compiler driver gcc 依序呼叫 cppcc1asld,把 .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
全章脈絡 (p.707)

本章以 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)
例外

某些版本的 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):

Important

兩大任務的細節(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
Tip

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 sizebyte 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 的格式字串、switchjump 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 字串
為什麼叫 .bss?(p.711 Aside)

源自 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 屬性的全域變數
C 的 static 屬性(p.713 New to C?)

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
Important

這個看似武斷的區分源於連結器做 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

銜接:符號解析的難點(p.715 開頭)


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.1x.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