Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Linker 與 Loader 完整指南

目錄

  1. 概述
  2. 程式編譯到執行的流程
  3. Linker(連結器)詳解
  4. Loader(載入器)詳解
  5. 與程式語言的關係
  6. 實作範例
  7. 常見問題與除錯
  8. 實際影響與效能考量

概述

Linker 和 Loader 是程式從原始碼到執行的關鍵環節。它們在編譯式語言中扮演著重要角色,負責將分散的程式碼模組組合成可執行的程式。

程式編譯到執行的流程

┌─────────────┐
│  原始碼.c   │
└──────┬──────┘
       │ 編譯器 (Compiler)
       ▼
┌─────────────┐
│ 組合語言.s  │
└──────┬──────┘
       │ 組譯器 (Assembler)
       ▼
┌─────────────┐
│  目的檔.o   │
└──────┬──────┘
       │ 連結器 (Linker)
       ▼
┌─────────────┐
│ 可執行檔.exe│
└──────┬──────┘
       │ 載入器 (Loader)
       ▼
┌─────────────┐
│ 記憶體執行  │
└─────────────┘

各階段說明

  1. 原始碼 (Source Code):開發者用高階語言撰寫的程式碼
  2. 編譯器 (Compiler):將原始碼轉換成組合語言或直接產生目的碼
  3. 組譯器 (Assembler):將組合語言轉換成機器碼(目的檔)
  4. 連結器 (Linker):將多個目的檔和函式庫結合成可執行檔
  5. 載入器 (Loader):將可執行檔載入記憶體並開始執行

Linker(連結器)詳解

主要功能

連結器負責將多個目的檔(.o 或 .obj)和函式庫結合成一個可執行檔。

1. 符號解析 (Symbol Resolution)

當程式中呼叫外部函式或變數時,連結器會找到這些符號的實際定義位置。

2. 位址重定位 (Relocation)

決定程式碼和資料在記憶體中的最終位置,並調整所有的位址參考。

3. 處理外部參考 (External References)

解決不同檔案之間的函式呼叫和變數參考。

連結類型

  • 靜態連結:將所有需要的程式碼都複製到可執行檔中

    • 優點:獨立執行,不需外部函式庫
    • 缺點:檔案較大,記憶體使用較多
  • 動態連結:執行時才載入共享函式庫

    • 優點:檔案較小,函式庫可共享
    • 缺點:需要確保系統有正確的函式庫版本

Loader(載入器)詳解

主要功能

載入器是作業系統的一部分,負責將程式載入記憶體並準備執行環境。

1. 分配記憶體

為程式的程式碼段、資料段、堆疊和堆積分配適當的記憶體空間。

2. 載入程式

將可執行檔從硬碟讀入分配好的記憶體區域。

3. 動態連結

處理動態函式庫(.dll、.so、.dylib)的載入和連結。

4. 初始化執行環境

設定程式計數器、堆疊指標等暫存器,準備程式執行。

與程式語言的關係

不同程式語言對 linker 和 loader 的依賴程度不同:

編譯式語言(C、C++、Rust)

完全依賴 linker 和 loader。你寫的程式必須經過連結才能產生可執行檔,必須經過載入才能執行。

直譯式語言(Python、JavaScript)

看似不需要 linker,但實際上:

  • 直譯器本身是經過連結的程式
  • 在載入外部模組時也有類似連結的過程
  • Python 的 import 和 JavaScript 的 require/import 都涉及動態載入

JIT 編譯語言(Java、C#)

有自己的載入和連結機制:

  • Java 的類別載入器(Class Loader)會在執行時期動態載入和連結類別
  • .NET 的 Assembly 載入機制處理組件的動態載入
  • 這些語言的虛擬機器(JVM、CLR)本身也是經過傳統連結的程式

實作範例

C 語言範例

讓我們用一個簡單的多檔案 C 程式來示範 linker 的工作。

math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 函式宣告
int add(int a, int b);
int multiply(int a, int b);

// 全域變數宣告
extern int global_counter;

#endif

math_utils.c

#include "math_utils.h"

// 全域變數定義
int global_counter = 0;

// 函式實作
int add(int a, int b) {
    global_counter++;
    return a + b;
}

int multiply(int a, int b) {
    global_counter++;
    return a * b;
}

main.c

#include <stdio.h>
#include "math_utils.h"

// 外部變數宣告
extern int global_counter;

int main() {
    int x = 10, y = 20;
    
    printf("加法: %d + %d = %d\n", x, y, add(x, y));
    printf("乘法: %d * %d = %d\n", x, y, multiply(x, y));
    printf("函式呼叫次數: %d\n", global_counter);
    
    return 0;
}

編譯與連結過程

# 步驟 1: 編譯成目的檔(不連結)
gcc -c main.c -o main.o
gcc -c math_utils.c -o math_utils.o

# 步驟 2: 連結成可執行檔
gcc main.o math_utils.o -o program

# 或者一步完成
gcc main.c math_utils.c -o program

# 執行程式
./program

查看符號表

# 查看目的檔的符號
nm main.o
# U 表示未定義(需要連結)
# T 表示定義在文字段(程式碼)
# D 表示定義在資料段

# 查看連結後的符號
nm program

C++ 範例

C++ 的連結過程涉及更複雜的符號管理,包括名稱修飾(name mangling)。

calculator.hpp

#ifndef CALCULATOR_HPP
#define CALCULATOR_HPP

#include <string>

class Calculator {
private:
    static int instance_count;  // 靜態成員
    std::string name;
    
public:
    Calculator(const std::string& calc_name);
    ~Calculator();
    
    // 內聯函式(定義在標頭檔)
    inline int quick_add(int a, int b) {
        return a + b;
    }
    
    // 一般成員函式(定義在 .cpp)
    double divide(double a, double b);
    
    // 靜態成員函式
    static int get_instance_count();
    
    // 模板函式(必須在標頭檔)
    template<typename T>
    T square(T value) {
        return value * value;
    }
};

// 模板類別(完全在標頭檔定義)
template<typename T>
class Storage {
private:
    T value;
public:
    Storage(T val) : value(val) {}
    T get() const { return value; }
};

#endif

calculator.cpp

#include "calculator.hpp"
#include <iostream>
#include <stdexcept>

// 靜態成員初始化(連結時需要)
int Calculator::instance_count = 0;

Calculator::Calculator(const std::string& calc_name) : name(calc_name) {
    instance_count++;
    std::cout << "建立 Calculator: " << name << std::endl;
}

Calculator::~Calculator() {
    instance_count--;
    std::cout << "銷毀 Calculator: " << name << std::endl;
}

double Calculator::divide(double a, double b) {
    if (b == 0) {
        throw std::runtime_error("除以零錯誤");
    }
    return a / b;
}

int Calculator::get_instance_count() {
    return instance_count;
}

main.cpp

#include <iostream>
#include "calculator.hpp"

// 使用外部函式庫
#include <cmath>  // 會連結數學函式庫

int main() {
    try {
        // 建立物件
        Calculator calc1("科學計算機");
        Calculator calc2("工程計算機");
        
        // 使用內聯函式
        std::cout << "5 + 3 = " << calc1.quick_add(5, 3) << std::endl;
        
        // 使用一般成員函式
        std::cout << "10 / 2 = " << calc1.divide(10, 2) << std::endl;
        
        // 使用模板函式
        std::cout << "7 的平方 = " << calc1.square(7) << std::endl;
        std::cout << "3.14 的平方 = " << calc1.square(3.14) << std::endl;
        
        // 使用靜態成員函式
        std::cout << "Calculator 實例數: " 
                  << Calculator::get_instance_count() << std::endl;
        
        // 使用模板類別
        Storage<int> int_storage(42);
        Storage<std::string> string_storage("Hello");
        
        std::cout << "整數儲存: " << int_storage.get() << std::endl;
        std::cout << "字串儲存: " << string_storage.get() << std::endl;
        
        // 使用數學函式庫
        std::cout << "sqrt(16) = " << sqrt(16) << std::endl;
        
    } catch (const std::exception& e) {
        std::cerr << "錯誤: " << e.what() << std::endl;
    }
    
    return 0;
}

編譯與連結

# 分開編譯
g++ -c main.cpp -o main.o
g++ -c calculator.cpp -o calculator.o

# 連結(包含標準函式庫和數學函式庫)
g++ main.o calculator.o -o calculator_app -lm

# 或一步完成
g++ main.cpp calculator.cpp -o calculator_app -lm

# 查看 C++ 的名稱修飾
nm calculator.o | c++filt

# 產生動態函式庫
g++ -shared -fPIC calculator.cpp -o libcalculator.so

# 使用動態函式庫連結
g++ main.cpp -L. -lcalculator -o calculator_app

Rust 範例

Rust 使用 cargo 管理編譯和連結過程,但底層仍然使用 linker。

建立專案結構

cargo new linker_demo --bin
cd linker_demo

src/math_ops.rs

#![allow(unused)]
fn main() {
// 模組定義
pub mod math_ops {
    // 靜態變數(類似 C 的全域變數)
    static mut OPERATION_COUNT: i32 = 0;
    
    // 公開結構
    pub struct Calculator {
        name: String,
    }
    
    impl Calculator {
        // 關聯函式(類似靜態方法)
        pub fn new(name: &str) -> Self {
            Calculator {
                name: name.to_string(),
            }
        }
        
        // 方法
        pub fn add(&self, a: i32, b: i32) -> i32 {
            unsafe {
                OPERATION_COUNT += 1;
            }
            println!("{}:執行加法", self.name);
            a + b
        }
        
        pub fn multiply(&self, a: i32, b: i32) -> i32 {
            unsafe {
                OPERATION_COUNT += 1;
            }
            println!("{}:執行乘法", self.name);
            a * b
        }
        
        // 取得操作次數
        pub fn get_operation_count() -> i32 {
            unsafe { OPERATION_COUNT }
        }
    }
    
    // 泛型函式(類似 C++ 模板)
    pub fn square<T>(value: T) -> T
    where
        T: std::ops::Mul<Output = T> + Copy,
    {
        value * value
    }
}

// 單元測試(會在測試時連結)
#[cfg(test)]
mod tests {
    use super::math_ops::*;
    
    #[test]
    fn test_calculator() {
        let calc = Calculator::new("測試計算機");
        assert_eq!(calc.add(2, 3), 5);
        assert_eq!(calc.multiply(4, 5), 20);
    }
    
    #[test]
    fn test_square() {
        assert_eq!(square(5), 25);
        assert_eq!(square(3.0), 9.0);
    }
}
}

src/lib.rs (如果要建立函式庫)

#![allow(unused)]
fn main() {
// 宣告模組
pub mod math_ops;

// 重新匯出
pub use math_ops::math_ops::Calculator;

// C 介面(用於與 C 程式連結)
#[no_mangle]
pub extern "C" fn rust_add(a: i32, b: i32) -> i32 {
    a + b
}

#[no_mangle]
pub extern "C" fn rust_multiply(a: i32, b: i32) -> i32 {
    a * b
}
}

src/main.rs

// 引入模組
mod math_ops;
use math_ops::math_ops::{Calculator, square};

// 使用外部 crate(會在連結時處理)
use std::collections::HashMap;

fn main() {
    println!("=== Rust Linker 示範 ===\n");
    
    // 建立計算機實例
    let calc1 = Calculator::new("計算機1");
    let calc2 = Calculator::new("計算機2");
    
    // 執行運算
    let x = 10;
    let y = 20;
    
    println!("結果:{} + {} = {}", x, y, calc1.add(x, y));
    println!("結果:{} * {} = {}", x, y, calc2.multiply(x, y));
    
    // 使用泛型函式
    println!("\n平方運算:");
    println!("整數 7 的平方 = {}", square(7));
    println!("浮點數 3.14 的平方 = {}", square(3.14));
    
    // 顯示操作次數
    println!("\n總操作次數:{}", Calculator::get_operation_count());
    
    // 使用標準函式庫(已連結)
    let mut map = HashMap::new();
    map.insert("加法結果", calc1.add(5, 3));
    map.insert("乘法結果", calc2.multiply(4, 6));
    
    println!("\n結果集合:");
    for (key, value) in &map {
        println!("  {} = {}", key, value);
    }
    
    // 使用條件編譯
    #[cfg(debug_assertions)]
    println!("\n[偵錯模式]");
    
    #[cfg(not(debug_assertions))]
    println!("\n[發布模式]");
}

Cargo.toml (套件配置)

[package]
name = "linker_demo"
version = "0.1.0"
edition = "2021"

# 相依套件(會在連結時處理)
[dependencies]

# 建構腳本(可選)
[build-dependencies]

# 函式庫設定
[lib]
name = "linker_demo"
crate-type = ["rlib", "cdylib", "staticlib"]

# 執行檔設定
[[bin]]
name = "linker_demo"
path = "src/main.rs"

# 優化設定會影響連結
[profile.release]
lto = true  # Link Time Optimization

編譯與連結命令

# 使用 cargo(自動處理連結)
cargo build          # 偵錯版本
cargo build --release  # 發布版本

# 查看編譯詳細資訊
cargo build -v

# 直接使用 rustc
rustc src/main.rs  # 單檔案編譯

# 分開編譯(產生 rlib)
rustc --crate-type=lib src/lib.rs
rustc -L . src/main.rs --extern linker_demo=liblinker_demo.rlib

# 產生靜態函式庫
rustc --crate-type=staticlib src/lib.rs -o liblinker_demo.a

# 產生動態函式庫
rustc --crate-type=cdylib src/lib.rs -o liblinker_demo.so

# 查看符號
nm target/debug/linker_demo

# 查看連結的動態函式庫
ldd target/debug/linker_demo

常見問題與除錯

1. 未定義符號錯誤 (Undefined Symbol)

C/C++ 錯誤訊息:

undefined reference to `function_name'

這就是 linker 在告訴你找不到某個函式或變數的定義。

原因與解決:

  • 忘記連結某個目的檔:確保所有 .o 檔都包含在連結命令中
  • 函式宣告與定義不符:檢查函式簽名是否一致
  • C++ 名稱修飾問題:使用 extern "C" 處理 C/C++ 混合編譯

2. 多重定義錯誤 (Multiple Definition)

錯誤訊息:

multiple definition of `variable_name'

原因與解決:

  • 在標頭檔定義變數:改用 extern 宣告,在 .c/.cpp 檔定義
  • 忘記使用 include guards:加入 #ifndef 保護
  • 內聯函式問題:確保內聯函式定義在標頭檔

3. 動態函式庫找不到

執行時錯誤:

error while loading shared libraries: libxxx.so: cannot open shared object file

解決方法:

# 設定函式庫路徑
export LD_LIBRARY_PATH=/path/to/library:$LD_LIBRARY_PATH

# 或安裝到系統路徑
sudo cp libxxx.so /usr/local/lib/
sudo ldconfig

4. Rust 特定問題

連結器找不到:

error: linker `cc` not found

解決:

# Ubuntu/Debian
sudo apt-get install build-essential

# macOS
xcode-select --install

# Windows
# 安裝 Visual Studio Build Tools

5. 檢查工具

Linux/macOS:

# 查看符號表
nm binary_file

# 查看動態連結
ldd binary_file  # Linux
otool -L binary_file  # macOS

# 查看段資訊
objdump -h binary_file

# 追蹤動態連結
strace ./program  # Linux
dtrace  # macOS

跨平臺:

# Rust 工具
cargo tree  # 查看相依關係
cargo rustc -- --print link-args  # 查看連結參數

實際影響與效能考量

瞭解 linker 和 loader 對程式設計很重要,因為它們會影響:

程式效能

靜態連結 vs 動態連結的選擇會直接影響程式的啟動速度和執行效能。

檔案大小

  • 靜態連結會讓執行檔變大(包含所有需要的程式碼)
  • 動態連結的執行檔較小(函式庫程式碼分離)

相依性管理

  • 靜態連結:無外部相依性,部署簡單
  • 動態連結:需要確保系統有正確版本的函式庫

除錯能力

連結錯誤是常見的編譯問題,瞭解連結過程有助於快速定位和解決問題。

靜態連結 vs 動態連結比較

靜態連結:

  • ✅ 載入速度快
  • ✅ 無相依性問題
  • ❌ 執行檔較大
  • ❌ 記憶體使用較多(無法共享)

動態連結:

  • ✅ 執行檔較小
  • ✅ 記憶體可共享
  • ✅ 可獨立更新函式庫
  • ❌ 載入速度較慢
  • ❌ 可能有版本相容問題

C/C++:

gcc -flto -O3 *.c -o program

Rust:

[profile.release]
lto = true  # 或 "thin" 或 "fat"

LTO 可以進行跨模組優化,提升效能但會增加編譯時間。

總結

Linker 和 Loader 是程式語言實作的重要部分,它們讓高階語言寫的程式能夠在實際硬體上執行:

  1. Linker 在編譯時期將分散的程式碼組合成可執行檔
  2. Loader 在執行時期將程式載入記憶體並建立執行環境
  3. 不同語言有不同的連結策略,但核心概念相同
  4. 即使你平常不直接接觸它們,瞭解它們的運作原理對理解程式的編譯和執行過程很有幫助

掌握這些概念後,你將能更好地:

  • 理解編譯錯誤訊息(如 "undefined reference")
  • 優化程式結構和效能
  • 處理函式庫相依性問題
  • 設計模組化的程式架構
  • 做出明智的技術決策(靜態 vs 動態連結)