摘要
連結器(Linker)是軟體開發工具鏈中的重要組件,負責將目標檔案(Object Files)、程式庫及其他二進位組件結合,產生可執行檔、共享程式庫或新的可重定位目標檔案。
在連結過程中,連結器必須處理符號解析(Symbol Resolution)、重定位(Relocation)、程式碼與資料區段的配置、程式庫相依性、程式進入點及作業系統載入程式所需的二進位結構。
連結器的發展與模組化程式設計密切相關。透過連結機制,開發者可以分別編譯多個原始碼檔案,並在最後階段將不同模組與程式庫整合為完整程式,而不必將所有原始碼視為單一編譯單位。
本文首先說明連結器的定義、歷史來源及其在編譯流程中的地位,接著分析目標檔案、符號表、符號解析與重定位機制,並討論靜態連結、動態連結、連結器腳本及連結階段最佳化。此外,本文比較 Linux、macOS 與 Windows 的連結機制,說明連結器與應用程式二進位介面(ABI)、ELF、Mach-O 及 PE/COFF 格式之間的關係。
關鍵詞: 連結器、編譯器、目標檔案、符號解析、重定位、靜態連結、動態連結、ELF、Mach-O、PE/COFF、應用程式二進位介面、連結階段最佳化。
一、什麼是連結器?
1.1. 定義
連結器(Linker)是一種處理目標檔案、程式庫及相關二進位資料,並將其整合為最終連結產物的程式。
依據輸出需求,連結產物可以是:
- 可執行檔(Executable File)。
- 共享程式庫(Shared Library)。
- 動態連結程式庫(Dynamic-Link Library)。
- 可供後續連結的目標檔案(Relocatable Object File)。
GNU Binutils 的《LD》文件對連結器的基本功能描述如下:
ld combines a number of object and archive files, relocates their data and ties up symbol references.
(中文譯文:ld 將多個目標檔案與封存檔結合,對其中的資料進行重定位,並解析其符號參照關係。)
資料來源:GNU Binutils,LD,第一節〈Overview〉。[1]
從技術本質而言,連結器必須解決一個基本問題:
當程式的不同部分分別編譯時,如何讓它們正確地參照彼此,並形成可以共同運作的二進位程式?
1.2. 為什麼需要連結器?
假設某個 C 語言程式由兩個原始碼檔案構成。
**math.c:**
int add(int a, int b) {
return a + b;
}
**main.c:**
int add(int a, int b);
int mainvoid {
return add(20, 22) != 42;
}
可以分別編譯:
gcc -c math.c -o math.o
gcc -c main.c -o main.o
得到:
math.o
main.o
其中,math.o 包含函式 add 的機器碼。
main.o 則包含函式 main 的機器碼,以及對 add 的呼叫。
然而,當 main.c 被獨立編譯時,編譯器未必知道 add 在最終可執行檔中的位置。
因此,目標檔案必須保留足夠的符號及重定位資訊,供後續連結使用。
執行:
gcc main.o math.o -o program
連結器便可確認 add 的定義位於 math.o,並依照目標格式的規則完成對應的符號參照與重定位。
這使程式能夠由多個獨立編譯的模組組成。
值得注意的是,上述 gcc 指令使用的是編譯器驅動程式(Compiler Driver)。它通常會自動加入必要的啟動檔案、程式庫及連結參數,因此不完全等同於直接呼叫 ld。
1.3. 連結器在編譯流程中的位置
傳統編譯流程可以簡化表示如下:
原始碼
main.c math.c
│ │
▼ ▼
編譯與程式碼產生
│ │
▼ ▼
main.o math.o
│ │
└─────┬──────┘
│
▼
連結器
Linker
│
▼
program
│
▼
作業系統載入
│
▼
程式執行
不同工具的職能如下:
| 組件 | 英文名稱 | 主要功能 |
|---|---|---|
| 編譯器 | Compiler | 將原始碼轉換為中介表示或機器碼 |
| 組譯器 | Assembler | 將組合語言轉換為目標檔案 |
| 連結器 | Linker | 整合目標檔案並處理跨模組參照 |
| 載入器 | Loader | 將程式映像建立於記憶體並準備執行 |
| 動態連結器 | Dynamic Linker | 依平台規則處理動態程式庫的載入與連結 |
實際工具鏈未必將這些階段完全分開。
例如,編譯器可以直接產生機器碼,而不必先輸出組合語言檔案;某些編譯器也可以內建部分或全部連結功能。
二、連結器的歷史來源與發展
2.1. 從單一程式到模組化設計
在早期電腦系統中,程式的編寫、記憶體配置與載入方式往往高度依賴特定硬體。
隨著程式規模擴大,要求程式設計者手動計算每個函式與資料的最終記憶體位址,逐漸變得難以管理。
此外,不同程式經常需要使用相同的數學函式、輸入輸出功能及其他共用程式碼。
如果每個程式都必須自行複製、調整及配置所有機器碼,將造成開發與維護上的困難。
因此,目標程式的重定位、模組連結及程式庫機制逐步形成。
歷史上,連結載入器(Linking Loader)、連結器及載入器的功能並不一定彼此獨立。有些系統會在載入程式時同時完成符號解析與重定位。
2.2. Unix 工具鏈與連結器
Unix 及 C 語言工具鏈的發展,使分離編譯與連結的模式被廣泛採用。
典型流程包括:
- 分別編譯原始碼。
- 產生目標檔案。
- 將多個目標檔案封裝成程式庫。
- 在最終階段連結應用程式。
a.out、COFF 及 ELF 等目標檔案格式,逐步提供機器碼、資料、符號及重定位資訊的組織方式。
在 GNU 工具鏈中,ld 成為重要的連結器,並可透過相關後端機制支援多種硬體架構與檔案格式。
2.3. 現代連結器的發展
現代大型軟體可能包含成千上萬個編譯單位、大量程式庫及龐大的除錯資訊。
因此,連結器面臨新的技術需求:
- 縮短連結時間。
- 支援不同處理器架構。
- 減少執行檔大小。
- 移除未使用的程式碼。
- 進行跨模組最佳化。
- 支援符號與除錯資訊。
- 提供跨平台及交叉連結能力。
- 配合作業系統的安全及載入機制。
目前具有代表性的連結器包括:
| 連結器 | 主要用途 |
|---|---|
GNU ld |
GNU 工具鏈中的連結器 |
LLVM lld |
LLVM 專案的連結器 |
Apple ld64 |
Apple 平台的 Mach-O 連結 |
Microsoft link.exe |
Windows 平台的 PE/COFF 連結 |
LLVM LLD 透過不同的實作部分支援 ELF、PE/COFF、Mach-O 及 WebAssembly 等格式。[2]
三、目標檔案與連結器需要處理的資料
3.1. 目標檔案(Object File)
目標檔案是編譯過程中的重要二進位產物。
它可能包含:
- 機器指令。
- 已初始化資料。
- 未初始化資料的配置資訊。
- 符號表。
- 重定位紀錄。
- 除錯資訊。
- 其他與平台相關的中繼資料。
例如:
gcc -c hello.c -o hello.o
hello.o 不一定可以直接執行,因為它可能仍然參照其他目標檔案或程式庫中的符號。
因此,連結器必須理解該檔案使用的二進位格式。
常見格式包括:
| 格式 | 主要使用環境 | 技術特徵 |
|---|---|---|
| ELF(Executable and Linkable Format) | Linux、部分 Unix 系統及嵌入式平台 | 節區、段、符號表與重定位 |
| Mach-O(Mach Object) | macOS、iOS 等 Apple 平台 | 標頭、載入命令、段與節區 |
| COFF/PE(Common Object File Format/Portable Executable) | Windows | COFF 目標檔案、PE 可執行映像及 DLL |
雖然三者具有部分相似的設計目的,但內部結構與規則並不相同。
3.2. 節區(Section)與段(Segment)
節區是用於組織目標檔案內容的單位。
以 ELF 為例:
| 節區 | 典型內容 |
|---|---|
.text |
機器指令 |
.rodata |
唯讀資料 |
.data |
已初始化且可寫入的資料 |
.bss |
未初始化資料的記憶體配置資訊 |
.symtab |
符號表 |
.strtab |
字串表 |
.rela.text |
特定情況下與程式碼有關的重定位紀錄 |
段則主要用於描述執行映像中需要由載入器處理或映射至記憶體的區域。
在 ELF 中,節區與段可視為兩種不同的檔案組織視角:
- 連結視角(Linking View)。
- 執行視角(Execution View)。
一個段可以包含來自多個節區的內容。
ELF 規格對這兩種視角及其資料結構均有明確說明。[3]
3.3. 符號表(Symbol Table)
符號表儲存連結器或二進位分析工具需要識別的名稱與相關資訊。
符號可能代表:
- 函式。
- 全域變數。
- 資料物件。
- 由連結器腳本定義的位置。
- 程式碼或資料中的特定位址。
例如:
int shared_value = 10;
int get_valuevoid {
return shared_value;
}
目標檔案可能包含 shared_value 與 get_value 的符號資訊。
這些資訊可以包括名稱、值、大小、類型、繫結屬性及所在節區。
四、符號解析(Symbol Resolution)
4.1. 已定義符號與未定義符號
假設程式包含兩個檔案。
**provider.c:**
int calculate(int x) {
return x * 2;
}
**consumer.c:**
extern int calculate(int x);
int processvoid {
return calculate(21);
}
分別編譯:
gcc -c provider.c -o provider.o
gcc -c consumer.c -o consumer.o
使用 nm:
nm provider.o
nm consumer.o
可能得到類似輸出:
provider.o:
T calculate
consumer.o:
U calculate
T process
在常見的 nm 輸出中:
T表示符號定義於對應的程式碼區域。U表示目前目標檔案中存在尚未定義的符號參照。
連結器的工作之一,就是將 consumer.o 中的 calculate 參照,與 provider.o 中相符的定義建立連結。
4.2. 符號的主要類型
以 ELF 為例,符號可依其繫結性質分為不同類型。
| 類型 | 英文名稱 | 說明 |
|---|---|---|
| 區域符號 | Local Symbol | 主要在所屬目標檔案內使用 |
| 全域符號 | Global Symbol | 可參與不同目標檔案之間的符號解析 |
| 弱符號 | Weak Symbol | 具有特定優先順序與解析規則的符號 |
| 未定義符號 | Undefined Symbol | 需要從其他單位取得定義的符號參照 |
符號解析規則與格式、ABI 及連結模式有關。
因此,不能假設 ELF、Mach-O 與 COFF 的弱符號機制完全一致。
4.3. 未定義符號錯誤
如果程式需要 calculate,但連結過程找不到合適的定義,可能出現:
undefined reference to `calculate`
常見原因包括:
- 缺少目標檔案。
- 未加入必要的程式庫。
- 符號名稱不一致。
- 目標檔案與架構不相容。
- 程式庫搜尋順序不符合預期。
- C++ 名稱修飾或 ABI 不一致。
但某些共享程式庫的連結模式允許保留部分未解析符號,待載入或執行時再處理。
是否允許此種行為,取決於平台與連結選項。
4.4. 重複定義符號
假設:
**a.c:**
int counter = 10;
**b.c:**
int counter = 20;
如果兩者均產生相同名稱的強全域定義,連結時通常會發生:
multiple definition of `counter`
實際處理方式仍取決於語言規則、符號性質及編譯器選項。
五、重定位(Relocation)
5.1. 為什麼需要重定位?
當原始碼被分別編譯時,編譯器通常無法得知整個程式的最終記憶體配置。
某個函式可能呼叫另一個檔案中的函式;某個指令也可能需要取得外部資料的位址。
因此,編譯器或組譯器會產生必要的重定位資訊。
連結器在確定最終配置後,依據這些資訊計算並修改相應的位址欄位或指令內容。
重定位的本質,是在確定符號與配置資訊後,完成原本尚無法確定的位址計算。
5.2. 相對位址的例子
假設連結器將兩個函式配置於:
caller: 0x00001000
target: 0x00001800
如果呼叫指令使用相對位址,連結器必須依照架構規格計算所需位移。
但不能直接認為所有情況都是:
target - caller
因為某些指令以目前指令位址為基準,另一些則可能以指令結束位置或重定位欄位位置為基準。
此外,部分架構還可能要求位移縮放、位元欄位編碼或範圍檢查。
5.3. 重定位運算的基本元素
在若干 ABI 規格中,常使用以下符號描述重定位公式:
| 符號 | 意義 |
|---|---|
| 被參照符號的值或位址 | |
| 加數(Addend) | |
| 重定位發生的位置 | |
| 載入基底位址,適用於特定重定位類型 |
相對位址重定位可能使用:
絕對位址重定位可能使用:
但這些僅是常見公式。
真正採用的運算方式、位元寬度、符號延伸與溢位檢查,必須依據處理器及 ABI 的具體重定位類型判定。
5.4. 重定位紀錄
ELF 的重定位資訊通常採用兩種形式:
REL
加數可能保存在需要修改的原始位置。
RELA
加數明確記錄於重定位項目中。
重定位紀錄通常包含:
- 需要修正的位置。
- 相關符號。
- 重定位類型。
- 必要時使用的加數。
不同處理器具有不同的重定位類型。
例如,x86-64 與 AArch64 的指令編碼方式不同,因此其重定位種類及計算規則也不相同。
5.5. 連結時重定位與載入時重定位
重定位不一定全部在建立執行檔時完成。
連結時重定位(Link-time Relocation)
由連結器在產生二進位輸出時完成。
載入時重定位(Load-time Relocation)
由載入器或動態連結器在程式載入記憶體時完成。
在部分系統中,符號解析或相關連結工作亦可能延後至第一次使用特定函式時。
這種工作分配是理解動態連結的重要基礎。
六、靜態連結(Static Linking)
6.1. 基本概念
靜態連結是指在建立程式時,從靜態程式庫中選取必要的機器碼及資料,並將其整合到連結產物中。
Unix 系統上的靜態程式庫通常使用 .a 副檔名。
Windows 上的 .lib 則可能是靜態程式庫,也可能是用於 DLL 的匯入程式庫(Import Library)。
因此,副檔名相同不代表其內容或用途完全相同。
6.2. 建立靜態程式庫
例如:
gcc -c math.c -o math.o
ar rcs libmath.a math.o
再進行連結:
gcc main.o ./libmath.a -o program
連結器可以從封存檔中選取符合未解析符號需求的目標檔案成員。
因此,靜態連結並不必然將整個程式庫的所有內容複製至執行檔。
實際選取與移除規則取決於連結器和相關最佳化選項。
6.3. 靜態連結的特徵
可能的優點包括:
- 減少執行時對部分外部程式庫的相依性。
- 便於在特定環境部署單一二進位程式。
- 適用於部分嵌入式或精簡系統。
可能的限制包括:
- 執行檔體積增加。
- 多個程式可能各自包含相同程式庫的程式碼。
- 更新程式庫可能需要重新連結與部署。
- 並不保證完全不依賴作業系統或執行階段環境。
6.4. 程式庫搜尋順序
在 GNU ld 的傳統封存檔搜尋模式中,目標檔案與程式庫在命令列上的順序可能影響結果。
例如:
gcc main.o -L. -lmath -o program
main.o 的未解析符號可以促使連結器從後方的 libmath.a 中選取適當成員。
但:
gcc -L. -lmath main.o -o program
在採用傳統單次封存檔搜尋規則的情況下,可能無法得到預期結果。
GNU ld 文件明確說明,封存檔通常在命令列出現的位置被搜尋,而不是在後續每次出現未解析符號時都自動重新搜尋。[1]
不同連結器或選項可能採用其他處理策略。
七、動態連結(Dynamic Linking)
7.1. 定義
動態連結允許程式在載入或執行階段,依照平台規定使用外部動態程式庫中的程式碼與資料。
常見形式:
| 作業系統 | 動態程式庫 |
|---|---|
| Linux | .so |
| macOS | .dylib |
| Windows | .dll |
在這種模式中,建置階段的連結器通常會記錄程式庫相依性,以及執行階段完成連結所需的資訊。
7.2. Linux 範例
建立共享程式庫:
gcc -fPIC -shared math.c -o libmath.so
連結主程式:
gcc main.o -L. -lmath \
-Wl,-rpath,'$ORIGIN' \
-o program
在支援該機制的 ELF 載入環境中,$ORIGIN 可用於表示與程式所在位置相關的路徑。
實際路徑搜尋規則仍取決於系統與載入環境。
7.3. 建置階段連結器與動態連結器
兩者雖然都可能被稱為 Linker,但其運作時間與工作內容不同。
| 項目 | 建置階段連結器 | 動態連結器 |
|---|---|---|
| 運作時間 | 建立二進位程式時 | 載入或執行程式時 |
| 主要輸入 | 目標檔案、程式庫與連結參數 | 執行映像、動態程式庫與載入資訊 |
| 主要工作 | 組合、符號解析、配置及重定位 | 載入相依程式庫並完成必要的動態解析 |
| 代表工具 | ld、ld.lld、ld64、link.exe |
ld.so、dyld 等平台機制 |
Windows 的 DLL 載入及匯入表解析主要由作業系統的載入機制處理,並不完全對應 Linux 的 ld.so 模型。
7.4. 動態連結的重要結構
在 ELF 系統中,動態連結可能涉及:
- 動態符號表(Dynamic Symbol Table)。
- 程式庫相依資訊。
- 動態重定位紀錄。
- 全域偏移表(Global Offset Table,GOT)。
- 程序連結表(Procedure Linkage Table,PLT)。
- 立即或延遲符號解析。
GOT 與 PLT 是許多 ELF 實作中的重要機制。
然而,並非所有動態函式呼叫都必須使用完全相同的 GOT/PLT 流程。
Mach-O 與 PE 亦具有自己的動態連結結構,不應直接套用 ELF 的全部術語。
7.5. 優點與限制
動態連結可能帶來:
- 多個行程共用同一程式庫的可共享記憶體頁面。
- 減少主程式執行檔的大小。
- 在 ABI 相容的條件下更新程式庫。
- 支援模組化與擴充式程式架構。
但也會引入:
- 程式庫遺失。
- 版本不相容。
- 符號衝突。
- ABI 不相容。
- 程式庫搜尋路徑問題。
- 不可信程式庫載入所造成的安全風險。
八、連結器腳本(Linker Script)與記憶體配置
8.1. 什麼是連結器腳本?
連結器腳本是描述或控制連結器如何配置節區、位址、記憶體區域及相關符號的設定檔案。
GNU ld 文件指出:
Every link is controlled by a linker script.
(中文譯文:每一次連結作業都由連結器腳本控制。)
在 GNU ld 中,如果沒有提供自訂腳本,連結器可以使用符合目前連結模式的預設腳本。[1]
8.2. 基本範例
ENTRYstart
SECTIONS
{
. = 0x10000;
.text : {
*(.text)
*(.text.*)
}
.rodata : {
*(.rodata)
*(.rodata.*)
}
.data : {
*(.data)
*(.data.*)
}
.bss : {
*(.bss)
*(.bss.*)
*COMMON
}
}
這是用於說明 GNU ld 語法的簡化範例,不代表所有平台都能直接使用的完整連結器腳本。
| 語法 | 功能 |
|---|---|
ENTRY |
指定程式進入點符號 |
SECTIONS |
定義輸出節區的配置 |
. |
位置計數器(Location Counter) |
.text |
配置機器指令 |
.rodata |
配置唯讀資料 |
.data |
配置已初始化資料 |
.bss |
配置未初始化資料所需空間 |
實際的可執行映像仍須符合目標系統對段、對齊、記憶體權限及 ABI 的要求。
8.3. 執行位址與載入位址
在部分嵌入式系統中,必須區分:
虛擬記憶體位址(Virtual Memory Address,VMA)
指程式執行時使用的位址。
載入記憶體位址(Load Memory Address,LMA)
指資料原先存放或被載入的位置。
例如,韌體可能將已初始化資料保存在 Flash 中,並於啟動時複製至 RAM。
連結器腳本可以定義相應的位址與符號,供啟動程式使用。
但資料複製的實際工作並非由連結器在執行階段自動完成,而是由啟動程式或載入機制負責。
8.4. 在作業系統與嵌入式系統中的用途
連結器腳本特別常見於:
- 作業系統核心。
- 開機載入程式(Bootloader)。
- 韌體(Firmware)。
- 裸機程式(Bare-metal Program)。
- 記憶體配置受到嚴格限制的系統。
- 自訂可執行映像的工具鏈。
在這些環境中,連結器所決定的記憶體配置往往直接影響程式能否正確啟動。
九、Linux、macOS 與 Windows 的連結機制
9.1. Linux:ELF
Linux 常使用 ELF 作為目標檔案與可執行檔格式。
連結器必須依照 ELF 及目標架構的 ABI,處理:
- 節區與段。
- 符號表。
- 重定位紀錄。
- 動態連結資料。
- 程式進入點。
- 程式標頭表。
GNU ld 與 LLVM ld.lld 是常見的 ELF 連結器。
9.2. macOS:Mach-O
macOS 與其他 Apple 平台廣泛使用 Mach-O 格式。
Mach-O 連結器需要建立或處理:
- Mach 標頭(Mach Header)。
- 載入命令(Load Commands)。
- 段與節區。
- 符號及重定位資訊。
- 動態連結相關資料。
- 平台要求的其他中繼資料。
Apple 使用 ld64 作為重要連結器,LLVM LLD 也具有 Mach-O 連結實作。[4]
此外,Mach-O 結構正確並不必然代表程式可以執行。
Apple 平台還可能要求程式碼簽署、符合平台安全政策,以及使用正確的目標平台資訊。
9.3. Windows:PE/COFF
Windows 通常使用 COFF 作為目標檔案格式,使用 PE 作為 EXE 與 DLL 等執行映像的格式。
連結器需要處理:
- COFF 符號。
- COFF 重定位。
- 節區配置。
- 匯入表(Import Table)。
- 匯出表(Export Table)。
- 基底重定位(Base Relocation)。
- PE 標頭及資料目錄。
Microsoft 的 PE/COFF 規格對上述結構有正式說明。[5]
常見連結器包括 Microsoft link.exe 與 LLVM lld-link。
9.4. 三種格式比較
| 比較項目 | ELF | Mach-O | PE/COFF |
|---|---|---|---|
| 代表平台 | Linux | macOS、Apple 平台 | Windows |
| 主要結構 | 節區、段、各種標頭 | 段、節區、載入命令 | 標頭、節區、資料目錄 |
| 符號機制 | ELF 符號表 | Mach-O 符號及動態連結資訊 | COFF 符號、匯入與匯出 |
| 重定位 | ELF 與架構 ABI | Mach-O 與目標架構規則 | COFF 重定位與 PE 基底重定位 |
| 常見連結器 | GNU ld、LLD | ld64、LLD Mach-O | link.exe、lld-link |
| 動態載入 | ELF 載入機制 | dyld | Windows Loader |
此表僅供概念比較。三種格式均具有更複雜的版本、架構及平台差異。
十、連結器的最佳化技術
10.1. 移除未使用的節區
連結器可以在符合規則的情況下,移除最終程式不需要的節區。
在 GNU ELF 工具鏈中,可以使用:
gcc -O2 -ffunction-sections -fdata-sections \
-Wl,--gc-sections \
main.c math.c -o program
其中:
-ffunction-sections:將適當的函式放入獨立節區。-fdata-sections:將適當的資料物件放入獨立節區。--gc-sections:要求連結器執行節區垃圾回收。
這些機制可以減少未使用程式碼及資料佔用的空間。
但部分節區可能受到進入點、保留規則或執行階段中繼資料要求的保護,不能任意刪除。
10.2. 相同程式碼折疊(Identical Code Folding)
某些連結器具有相同程式碼折疊(Identical Code Folding,ICF)功能。
此技術可將符合條件的等效程式碼節區合併,以減少最終二進位檔案的大小。
然而,即使兩個函式具有相同的機器碼,其位址是否可被合併,仍可能涉及程式可觀察行為或 ABI 的限制。
因此,ICF 必須遵守相應的安全與正確性要求。
10.3. 連結階段最佳化(Link-Time Optimization)
連結階段最佳化(LTO)允許編譯器在跨越多個編譯單位的範圍內分析與改善程式。
這類技術通常需要在目標檔案或其他中間產物中保留可供後續最佳化的程式表示。
例如:
gcc -O2 -flto -c math.c -o math.o
gcc -O2 -flto -c main.c -o main.o
gcc -O2 -flto main.o math.o -o program
可能進行的最佳化包括:
- 跨模組函式內聯。
- 常數傳播。
- 未使用函式消除。
- 程式碼特化。
- 控制流程簡化。
值得注意的是,LTO 通常涉及編譯器與連結器之間的協作,並不表示所有分析及最佳化工作都由連結器本身獨立完成。
10.4. 增量連結(Incremental Linking)
增量連結旨在重用先前連結的部分結果,以避免在輸入只發生少量變化時重新執行全部工作。
這對於大型軟體專案可能具有重要的建置效能價值。
但增量連結的支援程度,以及它與其他最佳化技術的相容性,依連結器而異。
十一、常見連結錯誤
11.1. 未定義符號
例如:
undefined reference to `foo`
可能原因包括缺少目標檔案、程式庫或符號名稱不一致。
可使用:
nm file.o
nm library.a
檢查符號定義及參照。
11.2. 重複定義
例如:
multiple definition of `counter`
需要檢查符號是否在多個編譯單位中被重複定義,以及其繫結方式是否正確。
11.3. 架構不相容
ARM64 程式不能直接將 x86-64 機器碼當作相同架構的目標檔案進行一般連結。
連結器必須檢查目標架構、檔案格式及相關 ABI。
11.4. 重定位超出範圍
部分指令所能表示的跳轉位移具有固定範圍。
如果目標位址超出該範圍,連結器可能報告重定位溢位或超出範圍。
在部分架構與連結器中,可以透過跳板程式碼(Thunk)或轉接段(Veneer)解決特定情況。
但並非所有超出範圍的重定位都能自動處理。
11.5. 動態程式庫無法載入
程式可能已成功連結,卻在執行時找不到必要的共享程式庫。
這種情況未必是建置階段連結器的錯誤,也可能與動態載入器的搜尋路徑或程式庫版本有關。
分析時應先確認錯誤發生的階段。
十二、實際操作與驗證
12.1. 建立目標檔案
建立 math.c:
int add(int a, int b) {
return a + b;
}
建立 main.c:
int add(int a, int b);
int mainvoid {
return add(20, 22) != 42;
}
分別編譯:
gcc -O0 -c math.c -o math.o
gcc -O0 -c main.c -o main.o
12.2. 檢查符號
nm math.o
nm main.o
觀察 math.o 是否定義 add,以及 main.o 是否具有對 add 的未定義參照。
12.3. 檢查重定位
在 ELF 平台:
readelf -r main.o
輸出可能包含與 add 呼叫相關的重定位紀錄。
具體類型依處理器架構與編譯器而異。
12.4. 完成連結
gcc main.o math.o -o program
接著:
nm program
objdump -d program
可以比較目標檔案與最終執行檔中的符號及機器碼。
如果使用 Mach-O 或 PE/COFF,應改用支援該格式的檢查工具,而非直接假設 readelf 適用。
12.5. 產生連結映射檔
在支援相關 GNU 連結器選項的環境中:
gcc main.o math.o \
-Wl,-Map=program.map \
-o program
產生的 program.map 可以協助觀察:
- 連結後的節區配置。
- 目標檔案與程式庫的配置情況。
- 符號位置。
- 記憶體區域與大小。
這類資訊對於韌體、核心及自訂執行環境的開發尤其重要。
十三、每個編譯器都必須使用獨立連結器嗎?
答案是否定的。
從系統架構的角度來看,編譯器可以自行完成傳統上屬於連結器的工作。
例如:
- 分析所有程式單位。
- 建立全程式符號資訊。
- 產生機器碼。
- 配置程式碼與資料。
- 計算及修正位址參照。
- 建立執行檔標頭。
- 輸出符合目標平台規範的二進位檔案。
在這種設計下,編譯器不一定需要呼叫外部 ld、lld 或其他連結器程式。
但是,系統仍然需要完成符號解析、位址配置及重定位等連結功能。
另一種設計是編譯器先完成部分連結工作,再將特定任務交由外部連結器處理。
因此,必須區分兩個概念:
- 連結器作為獨立工具。
- 連結作為程式建置過程中的一組必要功能。
獨立工具不是所有編譯器架構的強制要求,而相關連結功能是否必要,則取決於程式輸出形式與編譯系統設計。
十四、結論
連結器是現代軟體工具鏈中的重要組件,負責將獨立編譯的目標檔案及程式庫結合為完整的二進位產物。
符號解析與重定位是連結器的兩項基礎工作。除此之外,連結器還必須處理程式碼及資料配置、程式庫相依性、進入點、二進位檔案結構,以及部分最佳化任務。
從早期程式載入機制到現代 GNU ld、LLVM LLD、Apple ld64 及 Microsoft link.exe,連結技術隨著軟體規模、處理器架構與作業系統的發展而日益複雜。
理解連結器,亦必須理解目標檔案、符號表、重定位、ABI 及可執行檔格式之間的關係。
對編譯器設計而言,連結機制的重要性不僅在於產生最終執行檔,更在於提供跨模組組合與二進位互通的能力。
因此,連結器不應只被理解為編譯流程中的最後一個命令,而應被視為連接程式碼產生、二進位格式與程式執行環境的重要技術環節。
附錄一:技術術語
| 中文術語 | 英文術語 | 說明 |
|---|---|---|
| 連結器 | Linker | 整合目標檔案並產生連結產物的工具 |
| 目標檔案 | Object File | 含有機器碼、資料及連結資訊的二進位檔案 |
| 符號解析 | Symbol Resolution | 確定符號參照所對應的定義 |
| 重定位 | Relocation | 計算並修正程式碼或資料中的位址參照 |
| 符號表 | Symbol Table | 儲存符號名稱及相關資訊的結構 |
| 靜態連結 | Static Linking | 在建置時將所需程式碼整合至連結產物 |
| 動態連結 | Dynamic Linking | 在載入或執行階段完成相應的程式庫連結 |
| 載入器 | Loader | 在記憶體中建立程式執行映像的組件 |
| 動態連結器 | Dynamic Linker | 執行階段處理動態程式庫連結的組件 |
| 連結器腳本 | Linker Script | 控制節區及位址配置的設定檔案 |
| 進入點 | Entry Point | 程式開始執行的位置 |
| 增量連結 | Incremental Linking | 重用部分先前結果以縮短連結時間 |
| 連結階段最佳化 | Link-Time Optimization | 於連結階段進行跨編譯單位最佳化 |
| 應用程式二進位介面 | Application Binary Interface | 定義二進位組件互通規則的介面 |
| 靜態程式庫 | Static Library | 用於建置階段連結的目標檔案集合 |
| 共享程式庫 | Shared Library | 可由動態連結機制使用的程式庫 |
| 位置無關程式碼 | Position-Independent Code | 可依相應機制配置於不同載入位址的程式碼 |
附錄二:參考文獻
A. 連結器官方文件
[1] GNU Binutils, LD: The GNU Linker.
GNU ld 的官方文件,涵蓋符號解析、封存檔搜尋、重定位、連結器腳本及連結參數。
https://sourceware.org/binutils/docs/ld.html
[2] LLVM Project, LLD – The LLVM Linker.
LLVM LLD 的官方文件,介紹連結器的架構與 ELF、COFF、Mach-O 等格式支援。
[3] LLVM Project, The ELF, COFF and Wasm Linkers.
介紹 LLD 不同連結器實作的設計與技術背景。
https://releases.llvm.org/9.0.0/tools/lld/docs/NewLLD.html
[4] LLVM Project, Linker Script Implementation Notes and Policy.
說明 LLD 對 GNU 連結器腳本的支援與部分實作差異。
https://lld.llvm.org/ELF/linker_script.html
B. 二進位格式與 ABI 規格
[5] System V Application Binary Interface, Executable and Linking Format.
ELF 格式的重要規格文件,說明節區、段、符號表、重定位及程式載入相關結構。
https://refspecs.linuxfoundation.org/elf/elf.pdf
[6] Microsoft, Portable Executable and Common Object File Format Specification.
微軟的 PE/COFF 官方規格,包含標頭、符號、重定位、匯入表及匯出表等資料結構。
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
[7] Apple, Mach-O Documentation.
Apple 平台 Mach-O 二進位格式的相關技術文件。
https://developer.apple.com/documentation/kernel/mach-o
[8] LLVM Project, Mach-O LLD Port.
LLVM LLD 中 Mach-O 連結器實作的技術文件。
https://lld.llvm.org/MachO/index.html
C. 專書與延伸研究
[9] John R. Levine, Linkers and Loaders, Morgan Kaufmann, 1999.
專門研究連結器、載入器、符號解析、重定位及程式庫機制的經典著作。
[10] David A. Wheeler, Program Library HOWTO.
介紹 Unix 系統中靜態程式庫、共享程式庫及相關使用方式。
[11] Alfred V. Aho, Monica S. Lam, Ravi Sethi, Jeffrey D. Ullman, Compilers: Principles, Techniques, and Tools, Second Edition, Pearson, 2006.
探討編譯器架構、中介表示法、最佳化、程式碼產生及相關建置技術的基礎著作。
註釋
- GNU Binutils, LD: The GNU Linker,〈Overview〉、〈Linker Scripts〉及程式庫搜尋相關章節。https://sourceware.org/binutils/docs/ld.html ↩ ↩² ↩³
- LLVM Project, LLD – The LLVM Linker. https://lld.llvm.org/ ↩
- System V Application Binary Interface, Executable and Linking Format,關於連結視角、執行視角、節區及程式標頭的說明。https://refspecs.linuxfoundation.org/elf/elf.pdf ↩
- LLVM Project, Mach-O LLD Port. https://lld.llvm.org/MachO/index.html ↩
- Microsoft, PE Format,〈COFF Relocations〉、〈Import Table〉、〈Export Table〉及〈Base Relocations〉。https://learn.microsoft.com/en-us/windows/win32/debug/pe-format ↩